Resumen de las elecciones a la Comunidad de Madrid 2021¶
0.-Objetivo del presente trabajo¶
El presente trabajo tiene como objetivo analizar los resultados de las elecciones autonómicas del 4 de mayo del 2021 en Madrid y sacar conclusiones sobre la distribución del voto mediante representaciones espaciales, así como su comparación con pasadas elecciones para sacar conclusiones de cómo ha ido variando.
Nota sobre la obtención de los datos:¶
Los datos de las últimas elecciones se obtuvieron del siguiente enlace: https://resultados2021.comunidad.madrid/Mesas/es. Los pasos para analizar la información y procesarla están resumidos en el siguiente documento: Madrid_Elections_2021.ipynb . Al no conseguir datos similares con el mismo formato de pasadas elecciones autonómicas se optó por hacer un “web scrapper” de la página del periódico El País donde se resumen los votos obtenidos en las últimas 5 elecciones (2021, 2019, 2015, 2011 y 2007).
1.-Obtención y preparación de los datos¶
Obtenemos los datos de las elecciones autonómicas de Madrid 2019 y 2021 haciendo “scrapping” de las siguientes páginas:
https://resultados.elpais.com/elecciones/2019/autonomicas/12/,
https://resultados.elpais.com/elecciones/2021/autonomicas/12/
Para poder procesar la información de la web hemos tenido que instalar los siguientes paquetes:
Instalamos beautifulsoup4: pip install beautifulsoup4
Intalamos lxml: pip install lxml
Instalamos requests library: pip install requests
Analizando la web a la que nos lleva la primera url vemos el resumen de enlaces que a su vez llevan a los datos de cada municipio. Estos enlaces están alojados en elementos ‘ul’ que a su vez tienen una lista de elementos ‘li’ con enlaces que llevan a cada página.
Vamos a obtener la lista resumen de esos elementos ‘ul’, ‘li’ y los enlaces para después procesar cada página:
from bs4 import BeautifulSoup
import requests
1.0.-Preparación de los datos de 2019:¶
Obtenemos el texto html de la web y vemos que el elemento ul donde se aloja el resumen de municipios tiene la clase ‘estirar’:
html_text = requests.get('https://resultados.elpais.com/elecciones/2019/autonomicas/12/').text
soup = BeautifulSoup(html_text, 'lxml')
# soup
ul = soup.select('ul.estirar')[1]
# ul
El resumen de elementos ‘li’:
lis = ul.find_all('li')
# lis
Vamos a crear un array resumen que contenga el nombre de cada municipio y su link asociado:
url = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/'
results_2019 = []
for li in lis:
for link in li.find_all('a'):
local_result = {}
local_result['municipio'] = link.text
local_result['link'] = url+link.get('href')
results_2019.append(local_result)
results_2019[0]
{'municipio': 'Ajalvir',
'link': 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/02.html'}
1.0.0-Extracción de los datos de cada municipio¶
Vamos a hacer una prueba con una de las páginas donde se resumen los datos de un municipio para ver cómo tenemos que extraer los datos y después aplicarlo al resumen de municipios y links que hemos obtenido en results_2019.
Hacemos la prueba con el municipio de Madarcos:
# link de pruebas (municipio Madarcos):
link_pruebas = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/78.html'
# obtenemos el código html:
html_text_municipio = requests.get(link_pruebas).text
soup = BeautifulSoup(html_text_municipio, 'lxml')
# soup
Después de analizar dónde están los datos que buscamos podemos diferenciar dos tablas resumen con los siguientes identificadores:
Tabla de escrutado: ‘id’=’tablaResumen’
Tabla resumen partidos: ‘id’=’tablaVotosPartidos’
Tabla de escrutado:¶
Vemos que obtenemos los siguientes campos:
table_escrutado = soup.find('table', {'id': 'tablaResumen'})
table_escrutado_trs = table_escrutado.find_all('tr')
table_escrutado_trs
[<tr>
<th class="encabezado">Escrutado:</th>
<td class="tipoPorciento" colspan="2">100 %</td>
</tr>,
<tr>
<th class="encabezado">Votos contabilizados:</th>
<td class="tipoNumero">37</td>
<td class="tipoPorciento">92,5 %</td>
</tr>,
<tr>
<th class="encabezado">Abstenciones:</th>
<td class="tipoNumero">3</td>
<td class="tipoPorciento">7,5 %</td>
</tr>,
<tr>
<th class="encabezado">Votos nulos:</th>
<td class="tipoNumero">0</td>
<td class="tipoPorciento">0 %</td>
</tr>,
<tr>
<th class="encabezado">Votos en blanco:</th>
<td class="tipoNumero">0</td>
<td class="tipoPorciento">0 %</td>
</tr>]
Y que podemos resumir los datos de la siguiente manera:
results_table_escrutado = []
for tr in table_escrutado_trs:
local_table_escrutado = {}
local_table_escrutado['encabezado'] = None if tr.select_one('.encabezado') is None else tr.select_one('.encabezado').text
local_table_escrutado['numero'] = None if tr.select_one('.tipoNumero') is None else tr.select_one('.tipoNumero').text
local_table_escrutado['porcentaje'] = None if tr.select_one('.tipoPorciento') is None else tr.select_one('.tipoPorciento').text
results_table_escrutado.append(local_table_escrutado)
results_table_escrutado
[{'encabezado': 'Escrutado:', 'numero': None, 'porcentaje': '100 %'},
{'encabezado': 'Votos contabilizados:',
'numero': '37',
'porcentaje': '92,5 %'},
{'encabezado': 'Abstenciones:', 'numero': '3', 'porcentaje': '7,5 %'},
{'encabezado': 'Votos nulos:', 'numero': '0', 'porcentaje': '0 %'},
{'encabezado': 'Votos en blanco:', 'numero': '0', 'porcentaje': '0 %'}]
De aquí podemos aplicar unas funciones para extraer los datos de los campos comunes en cada diccionario (encabezado, número y porcentaje) y pasar los valores de string a numéricos:
def clean_strings_and_turn_float(value):
if ' %' in value:
return value.replace(' %', '').replace(',', '.')
else:
return value.replace('.', '')
def result_resume(results_table_escrutado):
results_resume = []
for result in results_table_escrutado:
local_resume = {}
if result.get('encabezado') == 'Escrutado:':
local_resume['escrutado'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Votos contabilizados:':
local_resume['votos_totales'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['votos_totales_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Abstenciones:':
local_resume['abstencion'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['abstencion_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Votos nulos:':
local_resume['votos_nulos'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['votos_nulos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Votos en blanco:':
local_resume['votos_blancos'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['votos_blancos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
results_resume.append(local_resume)
return results_resume
result_resume(results_table_escrutado)
[{'escrutado': '100'},
{'votos_totales': '37', 'votos_totales_porcentaje': '92.5'},
{'abstencion': '3', 'abstencion_porcentaje': '7.5'},
{'votos_nulos': '0', 'votos_nulos_porcentaje': '0'},
{'votos_blancos': '0', 'votos_blancos_porcentaje': '0'}]
Tabla resumen partidos:¶
Vemos que obtenemos los siguientes campos:
table_partidos = soup.find('table', {'id': 'tablaVotosPartidos'})
table_partido_trs = table_partidos.find_all('tr')
table_partido_trs
[<tr>
<th class="encabezado">Partido</th>
<th class="encabezado">Votos</th>
<th class="encabezado">%</th>
</tr>,
<tr><th class="nombrePartido"><acronym title="PARTIDO POPULAR">PP</acronym></th><td class="tipoNumeroVotos">15</td><td class="tipoPorcientoVotos">40,54 %</td></tr>,
<tr><th class="nombrePartido"><acronym title="UNIDAS PODEMOS">PODEMOS-IU</acronym></th><td class="tipoNumeroVotos">7</td><td class="tipoPorcientoVotos">18,92 %</td></tr>,
<tr><th class="nombrePartido"><acronym title="PARTIDO SOCIALISTA OBRERO ESPAÑOL">PSOE</acronym></th><td class="tipoNumeroVotos">6</td><td class="tipoPorcientoVotos">16,22 %</td></tr>,
<tr><th class="nombrePartido"><acronym title="CIUDADANOS-PARTIDO DE LA CIUDADANIA">Cs</acronym></th><td class="tipoNumeroVotos">3</td><td class="tipoPorcientoVotos">8,11 %</td></tr>,
<tr><th class="nombrePartido"><acronym title="MÁS MADRID">MÁS MADRID</acronym></th><td class="tipoNumeroVotos">3</td><td class="tipoPorcientoVotos">8,11 %</td></tr>,
<tr><th class="nombrePartido"><acronym title="VOX">VOX</acronym></th><td class="tipoNumeroVotos">2</td><td class="tipoPorcientoVotos">5,41 %</td></tr>,
<tr><th class="nombrePartido"><acronym title="PARTIDO ANIMALISTA CONTRA EL MALTRATO ANIMAL">PACMA</acronym></th><td class="tipoNumeroVotos">1</td><td class="tipoPorcientoVotos">2,7 %</td></tr>]
results_table_partido = []
for tr in table_partido_trs:
local_table_partido = {}
local_table_partido['partido'] = None if tr.select_one('.nombrePartido') is None else tr.select_one('.nombrePartido').text
local_table_partido['numero_votos'] = None if tr.select_one('.tipoNumeroVotos') is None else tr.select_one('.tipoNumeroVotos').text
local_table_partido['porcentaje'] = None if tr.select_one('.tipoPorcientoVotos') is None else tr.select_one('.tipoPorcientoVotos').text
results_table_partido.append(local_table_partido)
results_table_partido
[{'partido': None, 'numero_votos': None, 'porcentaje': None},
{'partido': 'PP', 'numero_votos': '15', 'porcentaje': '40,54 %'},
{'partido': 'PODEMOS-IU', 'numero_votos': '7', 'porcentaje': '18,92 %'},
{'partido': 'PSOE', 'numero_votos': '6', 'porcentaje': '16,22 %'},
{'partido': 'Cs', 'numero_votos': '3', 'porcentaje': '8,11 %'},
{'partido': 'MÁS MADRID', 'numero_votos': '3', 'porcentaje': '8,11 %'},
{'partido': 'VOX', 'numero_votos': '2', 'porcentaje': '5,41 %'},
{'partido': 'PACMA', 'numero_votos': '1', 'porcentaje': '2,7 %'}]
Vamos a hacer un tratamiento parecido al caso de la tabla de escrutado: vamos a reducir los nombres de cada partido a minúsculas, si tienen más de una palabra las unimos por guiones y eliminamos acentos; los valores los pasamos de strings a valores numéricos y eliminamos los signos de porcentajes.
import unicodedata
def strip_accents(text):
try:
text = unicode(text, 'utf-8')
except NameError: # unicode is a default on python 3
pass
text = unicodedata.normalize('NFD', text)\
.encode('ascii', 'ignore')\
.decode("utf-8")
return str(text)
def result_partido_resume(results_table_partido):
results_resume_partido = []
for result in results_table_partido:
local_resume = {}
if result.get('partido') == None:
continue
else:
partido = strip_accents(result.get('partido').lower().replace('-', '_').replace(' ', '_'))
local_resume[partido] = clean_strings_and_turn_float(result.get('numero_votos'))
local_resume[partido+'_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
results_resume_partido.append(local_resume)
return results_resume_partido
result_partido_resume(results_table_partido)
[{'pp': '15', 'pp_porcentaje': '40.54'},
{'podemos_iu': '7', 'podemos_iu_porcentaje': '18.92'},
{'psoe': '6', 'psoe_porcentaje': '16.22'},
{'cs': '3', 'cs_porcentaje': '8.11'},
{'mas_madrid': '3', 'mas_madrid_porcentaje': '8.11'},
{'vox': '2', 'vox_porcentaje': '5.41'},
{'pacma': '1', 'pacma_porcentaje': '2.7'}]
Ya podemos aplicar sobre todos los municipios. Para ellos definimos dos funciones que resumen lo que hemos hecho en ambas tablas:
def table_escrutado(link):
html_text_municipio = requests.get(link).text
soup = BeautifulSoup(html_text_municipio, 'lxml')
table_escrutado = soup.find('table', {'id': 'tablaResumen'})
table_escrutado_trs = table_escrutado.find_all('tr')
results_table_escrutado = []
for tr in table_escrutado_trs:
local_table_escrutado = {}
local_table_escrutado['encabezado'] = None if tr.select_one('.encabezado') is None else tr.select_one('.encabezado').text
local_table_escrutado['numero'] = None if tr.select_one('.tipoNumero') is None else tr.select_one('.tipoNumero').text
local_table_escrutado['porcentaje'] = None if tr.select_one('.tipoPorciento') is None else tr.select_one('.tipoPorciento').text
results_table_escrutado.append(local_table_escrutado)
return results_table_escrutado
def table_partido(link):
html_text_municipio = requests.get(link).text
soup = BeautifulSoup(html_text_municipio, 'lxml')
table_partido = soup.find('table', {'id': 'tablaVotosPartidos'})
table_partido_trs = table_partido.find_all('tr')
results_table_partido = []
for tr in table_partido_trs:
local_table_partido = {}
local_table_partido['partido'] = None if tr.select_one('.nombrePartido') is None else tr.select_one('.nombrePartido').text
local_table_partido['numero_votos'] = None if tr.select_one('.tipoNumeroVotos') is None else tr.select_one('.tipoNumeroVotos').text
local_table_partido['porcentaje'] = None if tr.select_one('.tipoPorcientoVotos') is None else tr.select_one('.tipoPorcientoVotos').text
results_table_partido.append(local_table_partido)
return results_table_partido
Aplicamos sobre la url inicial desde la que accederemos a todos los municipios:
url = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/'
results_pruebas = []
for li in lis:
for link in li.find_all('a'):
local_result = {}
local_result['municipio'] = link.text
local_result['link'] = url+link.get('href')
local_result['escrutinio'] = table_escrutado(local_result['link'])
local_result['partidos'] = table_partido(local_result['link'])
results_pruebas.append(local_result)
results_pruebas[0]
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
<ipython-input-15-3d2d3fa4870b> in <module>
6 local_result['municipio'] = link.text
7 local_result['link'] = url+link.get('href')
----> 8 local_result['escrutinio'] = table_escrutado(local_result['link'])
9 local_result['partidos'] = table_partido(local_result['link'])
10 results_pruebas.append(local_result)
<ipython-input-13-9e9d090d3b01> in table_escrutado(link)
1 def table_escrutado(link):
----> 2 html_text_municipio = requests.get(link).text
3 soup = BeautifulSoup(html_text_municipio, 'lxml')
4
5 table_escrutado = soup.find('table', {'id': 'tablaResumen'})
~/anaconda3/lib/python3.8/site-packages/requests/api.py in get(url, params, **kwargs)
74
75 kwargs.setdefault('allow_redirects', True)
---> 76 return request('get', url, params=params, **kwargs)
77
78
~/anaconda3/lib/python3.8/site-packages/requests/api.py in request(method, url, **kwargs)
59 # cases, and look like a memory leak in others.
60 with sessions.Session() as session:
---> 61 return session.request(method=method, url=url, **kwargs)
62
63
~/anaconda3/lib/python3.8/site-packages/requests/sessions.py in request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
540 }
541 send_kwargs.update(settings)
--> 542 resp = self.send(prep, **send_kwargs)
543
544 return resp
~/anaconda3/lib/python3.8/site-packages/requests/sessions.py in send(self, request, **kwargs)
653
654 # Send the request
--> 655 r = adapter.send(request, **kwargs)
656
657 # Total elapsed time of the request (approximately)
~/anaconda3/lib/python3.8/site-packages/requests/adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
437 try:
438 if not chunked:
--> 439 resp = conn.urlopen(
440 method=request.method,
441 url=url,
~/anaconda3/lib/python3.8/site-packages/urllib3/connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
697
698 # Make the request on the httplib connection object.
--> 699 httplib_response = self._make_request(
700 conn,
701 method,
~/anaconda3/lib/python3.8/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
443 # Python 3 (including for exceptions like SystemExit).
444 # Otherwise it looks like a bug in the code.
--> 445 six.raise_from(e, None)
446 except (SocketTimeout, BaseSSLError, SocketError) as e:
447 self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
~/anaconda3/lib/python3.8/site-packages/urllib3/packages/six.py in raise_from(value, from_value)
~/anaconda3/lib/python3.8/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
438 # Python 3
439 try:
--> 440 httplib_response = conn.getresponse()
441 except BaseException as e:
442 # Remove the TypeError from the exception chain in
~/anaconda3/lib/python3.8/http/client.py in getresponse(self)
1345 try:
1346 try:
-> 1347 response.begin()
1348 except ConnectionError:
1349 self.close()
~/anaconda3/lib/python3.8/http/client.py in begin(self)
305 # read until we get a non-100 response
306 while True:
--> 307 version, status, reason = self._read_status()
308 if status != CONTINUE:
309 break
~/anaconda3/lib/python3.8/http/client.py in _read_status(self)
266
267 def _read_status(self):
--> 268 line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
269 if len(line) > _MAXLINE:
270 raise LineTooLong("status line")
~/anaconda3/lib/python3.8/socket.py in readinto(self, b)
667 while True:
668 try:
--> 669 return self._sock.recv_into(b)
670 except timeout:
671 self._timeout_occurred = True
~/anaconda3/lib/python3.8/ssl.py in recv_into(self, buffer, nbytes, flags)
1239 "non-zero flags not allowed in calls to recv_into() on %s" %
1240 self.__class__)
-> 1241 return self.read(nbytes, buffer)
1242 else:
1243 return super().recv_into(buffer, nbytes, flags)
~/anaconda3/lib/python3.8/ssl.py in read(self, len, buffer)
1097 try:
1098 if buffer is not None:
-> 1099 return self._sslobj.read(len, buffer)
1100 else:
1101 return self._sslobj.read(len)
KeyboardInterrupt:
Vamos a chequear qué longitud tiene nuestra lista:
len(results_pruebas)
178
Vemos que tiene una longitud de 178 cuando se sabe que la Comunidad de Madrid tiene 179 municipios. Por lo tanto la página de inicio tiene un fallo y no presenta la información de un municipio.
Sabemos que el municipio que falta es La Acebeda. Más adelante cuando pasemos los resultados a un dataframe y le añadamos la información geográfica explicaremos cómo hemos sabido que era ese municipio.
Buscando de forma manual llegamos que la información electoral de La Acebeda para el año 2019 está resumida en este enlace: https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html
# insertamos la info. asociada a La Acebeda:
la_acebeda = {}
la_acebeda['municipio'] = 'La Acebeda'
la_acebeda['link'] = 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html'
la_acebeda['escrutinio'] = table_escrutado(la_acebeda['link'])
la_acebeda['partidos'] = table_partido(la_acebeda['link'])
results_pruebas.insert(0, la_acebeda)
results_pruebas[0]
{'municipio': 'La Acebeda',
'link': 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html',
'escrutinio': [{'encabezado': 'Escrutado:',
'numero': None,
'porcentaje': '100 %'},
{'encabezado': 'Votos contabilizados:',
'numero': '70',
'porcentaje': '90,91 %'},
{'encabezado': 'Abstenciones:', 'numero': '7', 'porcentaje': '9,09 %'},
{'encabezado': 'Votos nulos:', 'numero': '3', 'porcentaje': '4,29 %'},
{'encabezado': 'Votos en blanco:', 'numero': '0', 'porcentaje': '0 %'}],
'partidos': [{'partido': None, 'numero_votos': None, 'porcentaje': None},
{'partido': 'PP', 'numero_votos': '25', 'porcentaje': '37,31 %'},
{'partido': 'Cs', 'numero_votos': '14', 'porcentaje': '20,9 %'},
{'partido': 'PSOE', 'numero_votos': '11', 'porcentaje': '16,42 %'},
{'partido': 'MÁS MADRID', 'numero_votos': '10', 'porcentaje': '14,93 %'},
{'partido': 'PODEMOS-IU', 'numero_votos': '5', 'porcentaje': '7,46 %'},
{'partido': 'VOX', 'numero_votos': '2', 'porcentaje': '2,99 %'}]}
Pasamos a presentar la información de una forma que nos sea más fácil de tratar como dataframe:
results_pruebas_formatted = []
for result_prueba in results_pruebas:
local_result = {}
local_result['municipio'] = result_prueba['municipio']
local_result['link'] = result_prueba['link']
local_result['escrutinio'] = result_resume(result_prueba['escrutinio'])
local_result['partidos'] = result_partido_resume(result_prueba['partidos'])
results_pruebas_formatted.append(local_result)
results_pruebas_formatted[0]
{'municipio': 'La Acebeda',
'link': 'https://resultados.elpais.com/elecciones/2019/autonomicas/12/28/01.html',
'escrutinio': [{'escrutado': '100'},
{'votos_totales': '70', 'votos_totales_porcentaje': '90.91'},
{'abstencion': '7', 'abstencion_porcentaje': '9.09'},
{'votos_nulos': '3', 'votos_nulos_porcentaje': '4.29'},
{'votos_blancos': '0', 'votos_blancos_porcentaje': '0'}],
'partidos': [{'pp': '25', 'pp_porcentaje': '37.31'},
{'cs': '14', 'cs_porcentaje': '20.9'},
{'psoe': '11', 'psoe_porcentaje': '16.42'},
{'mas_madrid': '10', 'mas_madrid_porcentaje': '14.93'},
{'podemos_iu': '5', 'podemos_iu_porcentaje': '7.46'},
{'vox': '2', 'vox_porcentaje': '2.99'}]}
1.0.1-Convertir la información en dataframe¶
Vamos a crear un dataframe donde se muestre la información de cada municipio y el resumen de partidos a los que se ha votado en cada uno.
import pandas as pd
df_partidos = pd.json_normalize(results_pruebas_formatted, record_path='partidos', meta=['municipio', 'link'])
df_partidos = df_partidos.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)
df_escrutinio = pd.json_normalize(results_pruebas_formatted, record_path='escrutinio', meta=['municipio'])
df_escrutinio = df_escrutinio.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)
df = pd.merge(df_partidos, df_escrutinio, on='municipio', how='outer')
df
| pp | pp_porcentaje | cs | cs_porcentaje | psoe | psoe_porcentaje | mas_madrid | mas_madrid_porcentaje | podemos_iu | podemos_iu_porcentaje | ... | link | escrutado | votos_totales | votos_totales_porcentaje | abstencion | abstencion_porcentaje | votos_nulos | votos_nulos_porcentaje | votos_blancos | votos_blancos_porcentaje | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 521 | 24.05 | 456 | 21.05 | 532 | 24.56 | 224 | 10.34 | 81 | 3.74 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 2178 | 66.34 | 1105 | 33.66 | 12 | 0.55 | 27 | 1.25 |
| 1 | 41 | 29.29 | 19 | 13.57 | 54 | 38.57 | 12 | 8.57 | 3 | 2.14 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 140 | 82.84 | 29 | 17.16 | 0 | 0 | 0 | 0 |
| 2 | 15953 | 17.91 | 18120 | 20.34 | 28759 | 32.28 | 10975 | 12.32 | 5306 | 5.96 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 89514 | 65.71 | 46719 | 34.29 | 434 | 0.48 | 424 | 0.48 |
| 3 | 13844 | 25.38 | 11032 | 20.22 | 15338 | 28.12 | 6047 | 11.08 | 2527 | 4.63 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 54711 | 67.79 | 25992 | 32.21 | 157 | 0.29 | 189 | 0.35 |
| 4 | 16844 | 19.29 | 16214 | 18.57 | 27492 | 31.48 | 12227 | 14 | 6537 | 7.49 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 87720 | 69.08 | 39267 | 30.92 | 387 | 0.44 | 424 | 0.49 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 174 | 249 | 26.38 | 175 | 18.54 | 229 | 24.26 | 105 | 11.12 | 53 | 5.61 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 946 | 65.33 | 502 | 34.67 | 2 | 0.21 | 2 | 0.21 |
| 175 | 778 | 20.24 | 839 | 21.83 | 1488 | 38.71 | 261 | 6.79 | 147 | 3.82 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 3882 | 73.2 | 1421 | 26.8 | 38 | 0.98 | 31 | 0.81 |
| 176 | 4628 | 30.76 | 3416 | 22.7 | 2762 | 18.36 | 1508 | 10.02 | 523 | 3.48 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 15105 | 72.93 | 5606 | 27.07 | 59 | 0.39 | 60 | 0.4 |
| 177 | 40 | 24.54 | 23 | 14.11 | 47 | 28.83 | 18 | 11.04 | 16 | 9.82 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 163 | 82.32 | 35 | 17.68 | 0 | 0 | 0 | 0 |
| 178 | 193 | 23.89 | 77 | 9.53 | 244 | 30.2 | 102 | 12.62 | 109 | 13.49 | ... | https://resultados.elpais.com/elecciones/2019/... | 100 | 821 | 72.27 | 315 | 27.73 | 13 | 1.58 | 3 | 0.37 |
179 rows × 41 columns
# cambiamos la posición de las columnas para una mejor presentación:
first_column = df.pop('municipio')
second_column = df.pop('link')
df.insert(0, 'municipio', first_column)
df.insert(1, 'link', second_column)
df
| municipio | link | pp | pp_porcentaje | cs | cs_porcentaje | psoe | psoe_porcentaje | mas_madrid | mas_madrid_porcentaje | ... | uleg_porcentaje | escrutado | votos_totales | votos_totales_porcentaje | abstencion | abstencion_porcentaje | votos_nulos | votos_nulos_porcentaje | votos_blancos | votos_blancos_porcentaje | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Ajalvir | https://resultados.elpais.com/elecciones/2019/... | 521 | 24.05 | 456 | 21.05 | 532 | 24.56 | 224 | 10.34 | ... | NaN | 100 | 2178 | 66.34 | 1105 | 33.66 | 12 | 0.55 | 27 | 1.25 |
| 1 | Alameda del Valle | https://resultados.elpais.com/elecciones/2019/... | 41 | 29.29 | 19 | 13.57 | 54 | 38.57 | 12 | 8.57 | ... | NaN | 100 | 140 | 82.84 | 29 | 17.16 | 0 | 0 | 0 | 0 |
| 2 | Alcalá de Henares | https://resultados.elpais.com/elecciones/2019/... | 15953 | 17.91 | 18120 | 20.34 | 28759 | 32.28 | 10975 | 12.32 | ... | 0.01 | 100 | 89514 | 65.71 | 46719 | 34.29 | 434 | 0.48 | 424 | 0.48 |
| 3 | Alcobendas | https://resultados.elpais.com/elecciones/2019/... | 13844 | 25.38 | 11032 | 20.22 | 15338 | 28.12 | 6047 | 11.08 | ... | 0.03 | 100 | 54711 | 67.79 | 25992 | 32.21 | 157 | 0.29 | 189 | 0.35 |
| 4 | Alcorcón | https://resultados.elpais.com/elecciones/2019/... | 16844 | 19.29 | 16214 | 18.57 | 27492 | 31.48 | 12227 | 14 | ... | 0.02 | 100 | 87720 | 69.08 | 39267 | 30.92 | 387 | 0.44 | 424 | 0.49 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 174 | Villar del Olmo | https://resultados.elpais.com/elecciones/2019/... | 249 | 26.38 | 175 | 18.54 | 229 | 24.26 | 105 | 11.12 | ... | NaN | 100 | 946 | 65.33 | 502 | 34.67 | 2 | 0.21 | 2 | 0.21 |
| 175 | Villarejo de Salvanés | https://resultados.elpais.com/elecciones/2019/... | 778 | 20.24 | 839 | 21.83 | 1488 | 38.71 | 261 | 6.79 | ... | NaN | 100 | 3882 | 73.2 | 1421 | 26.8 | 38 | 0.98 | 31 | 0.81 |
| 176 | Villaviciosa de Odón | https://resultados.elpais.com/elecciones/2019/... | 4628 | 30.76 | 3416 | 22.7 | 2762 | 18.36 | 1508 | 10.02 | ... | 0.02 | 100 | 15105 | 72.93 | 5606 | 27.07 | 59 | 0.39 | 60 | 0.4 |
| 177 | Villavieja del Lozoya | https://resultados.elpais.com/elecciones/2019/... | 40 | 24.54 | 23 | 14.11 | 47 | 28.83 | 18 | 11.04 | ... | NaN | 100 | 163 | 82.32 | 35 | 17.68 | 0 | 0 | 0 | 0 |
| 178 | Zarzalejo | https://resultados.elpais.com/elecciones/2019/... | 193 | 23.89 | 77 | 9.53 | 244 | 30.2 | 102 | 12.62 | ... | NaN | 100 | 821 | 72.27 | 315 | 27.73 | 13 | 1.58 | 3 | 0.37 |
179 rows × 41 columns
df.count()
municipio 179
link 179
pp 179
pp_porcentaje 179
cs 179
cs_porcentaje 179
psoe 179
psoe_porcentaje 179
mas_madrid 178
mas_madrid_porcentaje 178
podemos_iu 179
podemos_iu_porcentaje 179
vox 179
vox_porcentaje 179
pacma 159
pacma_porcentaje 159
fe_de_las_jons 134
fe_de_las_jons_porcentaje 134
pum+j 120
pum+j_porcentaje 120
pcte 113
pcte_porcentaje 113
upyd 120
upyd_porcentaje 120
ph 96
ph_porcentaje 96
pcas_tc 95
pcas_tc_porcentaje 95
p_lib 90
p_lib_porcentaje 90
uleg 71
uleg_porcentaje 71
escrutado 179
votos_totales 179
votos_totales_porcentaje 179
abstencion 179
abstencion_porcentaje 179
votos_nulos 179
votos_nulos_porcentaje 179
votos_blancos 179
votos_blancos_porcentaje 179
dtype: int64
1.0.2-Añadir información geoespacial al dataframe:¶
La información geoespacial la hemos obtenido del siguiente enlace: https://raw.githubusercontent.com/FMullor/TopoJson/master/MadridMunicipios.geojson.
Nos va a servir para poder realizar mapas de la distribución del voto en cada municipio.
import geopandas as gpd
import matplotlib.pyplot as plt
# sacamos la info del link y ordenamos por orden alfabético los municipios:
municipios = 'https://raw.githubusercontent.com/FMullor/TopoJson/master/MadridMunicipios.geojson'
map_municipios = gpd.read_file(municipios)
map_municipios = map_municipios.sort_values('municipio')
map_municipios.head()
| id_0 | iso | pais | id_1 | communidad_ | id_2 | provincia | id_3 | name_3 | id_4 | ... | varname_4 | ccn_4 | cca_4 | type_4 | engtype_4 | cpro | cmun | dc | codigo_post | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 156 | 215.0 | ESP | Spain | 8.0 | Comunidad de Madrid | 33.0 | Madrid | 234.0 | n.a. (176) | 5876.0 | ... | None | 0.0 | None | Municipality | Municipality | 28 | 002 | 9 | 28002 | MULTIPOLYGON (((-3.51150 40.53889, -3.50521 40... |
| 58 | 215.0 | ESP | Spain | 8.0 | Comunidad de Madrid | 33.0 | Madrid | 233.0 | n.a. (175) | 5828.0 | ... | None | 0.0 | None | Municipality | Municipality | 28 | 003 | 5 | 28003 | MULTIPOLYGON (((-3.80596 40.89047, -3.80951 40... |
| 86 | 215.0 | ESP | Spain | 8.0 | Comunidad de Madrid | 33.0 | Madrid | 234.0 | n.a. (176) | 5877.0 | ... | None | 0.0 | None | Municipality | Municipality | 28 | 005 | 3 | 28005 | MULTIPOLYGON (((-3.32142 40.47207, -3.32898 40... |
| 168 | 215.0 | ESP | Spain | 8.0 | Comunidad de Madrid | 33.0 | Madrid | 235.0 | n.a. (177) | 5907.0 | ... | None | 0.0 | None | Municipality | Municipality | 28 | 006 | 6 | 28006 | MULTIPOLYGON (((-3.67417 40.58897, -3.65981 40... |
| 154 | 215.0 | ESP | Spain | 8.0 | Comunidad de Madrid | 33.0 | Madrid | 235.0 | n.a. (177) | 5908.0 | ... | None | 0.0 | None | Municipality | Municipality | 28 | 007 | 2 | 28007 | MULTIPOLYGON (((-3.78781 40.35875, -3.79893 40... |
5 rows × 21 columns
map_municipios.info()
<class 'geopandas.geodataframe.GeoDataFrame'>
Int64Index: 182 entries, 156 to 70
Data columns (total 21 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id_0 182 non-null float64
1 iso 182 non-null object
2 pais 182 non-null object
3 id_1 182 non-null float64
4 communidad_ 182 non-null object
5 id_2 182 non-null float64
6 provincia 182 non-null object
7 id_3 182 non-null float64
8 name_3 182 non-null object
9 id_4 182 non-null float64
10 municipio 182 non-null object
11 varname_4 2 non-null object
12 ccn_4 182 non-null float64
13 cca_4 0 non-null object
14 type_4 182 non-null object
15 engtype_4 182 non-null object
16 cpro 164 non-null object
17 cmun 164 non-null object
18 dc 164 non-null object
19 codigo_post 164 non-null object
20 geometry 182 non-null geometry
dtypes: float64(6), geometry(1), object(14)
memory usage: 31.3+ KB
Como podemos ver, el geodataframe map_municipios tiene como máximo 182 registros. A la hora de poder cruzar la información con nuestros dataframes, vemos que la columna ‘municipio’ tiene 182 registros, lo que significa que hay 3 elementos de más sabiendo que la Comunidad de Madrid tiene 179 municipios. Tenemos que investigar si hay elementos repetitivos y cuáles son. Una vez limpio, solo nos interesan las columnas ‘municipio’ y ‘geometry’:
map_municipios = map_municipios[['municipio', 'geometry']]
map_municipios
| municipio | geometry | |
|---|---|---|
| 156 | Ajalvir | MULTIPOLYGON (((-3.51150 40.53889, -3.50521 40... |
| 58 | Alameda del Valle | MULTIPOLYGON (((-3.80596 40.89047, -3.80951 40... |
| 86 | Alcalá de Henares | MULTIPOLYGON (((-3.32142 40.47207, -3.32898 40... |
| 168 | Alcobendas | MULTIPOLYGON (((-3.67417 40.58897, -3.65981 40... |
| 154 | Alcorcón | MULTIPOLYGON (((-3.78781 40.35875, -3.79893 40... |
| ... | ... | ... |
| 166 | Villar del Olmo | MULTIPOLYGON (((-3.21619 40.31261, -3.22639 40... |
| 175 | Villarejo de Salvanés | MULTIPOLYGON (((-3.20168 40.08538, -3.20231 40... |
| 173 | Villaviciosa de Odón | MULTIPOLYGON (((-4.00598 40.33916, -3.99856 40... |
| 178 | Villavieja del Lozoya | MULTIPOLYGON (((-3.65536 41.00760, -3.65717 41... |
| 70 | Zarzalejo | MULTIPOLYGON (((-4.15354 40.53144, -4.16337 40... |
182 rows × 2 columns
Vemos que hay municipios cuyo nombre al tener acentos o caracteres especiales no aparece correctamente y no va a coincidir con la información de nuestro dataframe. Para obtenerlos, hacemos:
no_intersection_map = set(map_municipios['municipio']).difference(set(df['municipio']))
len(no_intersection_map)
no_intersection_map
{'Alcalá de Henares',
'Alcorcón',
'Carabaña',
'ChapinerÃ\xada',
'Chinchón',
'Cobeña',
'El Vellón',
'El Ã\x81lamo',
'Fuentidueña de Tajo',
'Griñón',
'Horcajo de la Sierra',
'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
'Jurisdicción Mancomunada de Cerdedilla y Navacerrada',
'Leganés',
'Morata de Tajuña',
'Móstoles',
'Navarredonda y San Mamés',
'Nuevo Baztán',
'Orusco de Tajuña',
'Perales de Tajuña',
'Pinuécar-Gandullas',
'Pozuelo de Alarcón',
'Prádena del Rincón',
'RascafrÃ\xada',
'Redueña',
'San AgustÃ\xadn del Guadalix',
'San MartÃ\xadn de Valdeiglesias',
'San MartÃ\xadn de la Vega',
'San Sebastián de los Reyes',
'Santa MarÃ\xada de la Alameda',
'Torrejón de Ardoz',
'Torrejón de Velasco',
'Torrejón de la Calzada',
'Valdepiélagos',
'Valverde de Alcalá',
'Villanueva de la Cañada',
'Villarejo de Salvanés',
'Villaviciosa de Odón'}
Por lo tanto, procedemos a corregir los nombres:
map_municipios['municipio'] = map_municipios['municipio'].replace([
'Alcalá de Henares',
'Alcorcón',
'Carabaña',
'ChapinerÃ\xada',
'Chinchón',
'Cobeña',
'El Vellón',
'El Ã\x81lamo',
'Fuentidueña de Tajo',
'Griñón',
'Horcajo de la Sierra',
'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
'Jurisdicción Mancomunada de Cerdedilla y Navacerrada',
'Leganés',
'Morata de Tajuña',
'Móstoles',
'Navarredonda y San Mamés',
'Nuevo Baztán',
'Orusco de Tajuña',
'Perales de Tajuña',
'Pinuécar-Gandullas',
'Pozuelo de Alarcón',
'Prádena del Rincón',
'RascafrÃ\xada',
'Redueña',
'San AgustÃ\xadn del Guadalix',
'San MartÃ\xadn de Valdeiglesias',
'San MartÃ\xadn de la Vega',
'San Sebastián de los Reyes',
'Santa MarÃ\xada de la Alameda',
'Torrejón de Ardoz',
'Torrejón de Velasco',
'Torrejón de la Calzada',
'Valdepiélagos',
'Valverde de Alcalá',
'Villanueva de la Cañada',
'Villarejo de Salvanés',
'Villaviciosa de Odón'
], [
'Alcalá de Henares',
'Alcorcón',
'Carabaña',
'Chapinería',
'Chinchón',
'Cobeña',
'El Vellón',
'El Álamo',
'Fuentidueña de Tajo',
'Griñón',
'Horcajo de la Sierra',
'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
'Jurisdicción Mancomunada de Cerdedilla y Navacerrada',
'Leganés',
'Morata de Tajuña',
'Móstoles',
'Navarredonda y San Mamés',
'Nuevo Baztán',
'Orusco de Tajuña',
'Perales de Tajuña',
'Piñuécar-Gandullas',
'Pozuelo de Alarcón',
'Prádena del Rincón',
'Rascafría',
'Redueña',
'San Agustín del Guadalix',
'San Martín de Valdeiglesias',
'San Martín de la Vega',
'San Sebastián de los Reyes',
'Santa María de la Alameda',
'Torrejón de Ardoz',
'Torrejón de Velasco',
'Torrejón de la Calzada',
'Valdepiélagos',
'Valverde de Alcalá',
'Villanueva de la Cañada',
'Villarejo de Salvanés',
'Villaviciosa de Odón'])
map_municipios.head()
| municipio | geometry | |
|---|---|---|
| 156 | Ajalvir | MULTIPOLYGON (((-3.51150 40.53889, -3.50521 40... |
| 58 | Alameda del Valle | MULTIPOLYGON (((-3.80596 40.89047, -3.80951 40... |
| 86 | Alcalá de Henares | MULTIPOLYGON (((-3.32142 40.47207, -3.32898 40... |
| 168 | Alcobendas | MULTIPOLYGON (((-3.67417 40.58897, -3.65981 40... |
| 154 | Alcorcón | MULTIPOLYGON (((-3.78781 40.35875, -3.79893 40... |
Vemos si hay valores repetidos:
# sacamos los nombres de los municipios y los metemos en una lista:
municipios = map_municipios['municipio'].to_list()
#comprobamos qué nombres de municipios pueden estar repetidos:
set([x for x in municipios if municipios.count(x) > 1])
{'Arroyomolinos'}
Vemos si los valores de ‘geometry’ de ‘Arroyomolinos’ son los mismos y por lo tanto se pueden eliminar sin problema:
map_municipios[map_municipios['municipio'] == 'Arroyomolinos']
| municipio | geometry | |
|---|---|---|
| 60 | Arroyomolinos | MULTIPOLYGON (((-3.94179 40.29215, -3.93664 40... |
| 89 | Arroyomolinos | MULTIPOLYGON (((-3.94179 40.29215, -3.93664 40... |
Son los mismos, se puede eliminar un registro:
# nos quedamos solo con un valor de Arroyomolinos (keep = 'first'):
map_municipios.drop_duplicates(subset ="municipio", keep = 'first', inplace = True)
map_municipios[map_municipios['municipio'] == 'Arroyomolinos']
| municipio | geometry | |
|---|---|---|
| 60 | Arroyomolinos | MULTIPOLYGON (((-3.94179 40.29215, -3.93664 40... |
Vemos si pueden haber diferencias entre los nombres de los municipios:
no_intersection_map = set(map_municipios['municipio']).difference(set(df['municipio']))
len(no_intersection_map)
no_intersection_map
{'Horcajo de la Sierra',
'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
'Jurisdicción Mancomunada de Cerdedilla y Navacerrada'}
Comprobamos con qué nombres se corresponderían estos municipios de map_municipios con los que tenemos en nuestro dataframe:
Horcajo de la Sierra : Horcajo de la Sierra-Aoslos,
Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral) : Manzanares el Real,
Jurisdicción Mancomunada de Cerdedilla y Navacerrada : Navacerrada
Antes de corregir los nombres, comprobamos si ya existen en map_municipios:
map_municipios[map_municipios['municipio'] == 'Horcajo de la Sierra-Aoslos']
| municipio | geometry |
|---|
map_municipios[map_municipios['municipio'] == 'Manzanares el Real']
| municipio | geometry | |
|---|---|---|
| 12 | Manzanares el Real | MULTIPOLYGON (((-3.90600 40.68217, -3.90882 40... |
map_municipios[map_municipios['municipio'] == 'Navacerrada']
| municipio | geometry | |
|---|---|---|
| 171 | Navacerrada | MULTIPOLYGON (((-3.96854 40.76714, -3.97736 40... |
Se ve que ‘Horcajo de la Sierra-Aoslos’ es el único que no existe, por lo tanto se puede reemplazar.
Tenemos que ver si los otros dos pares de nombres de municipios tienen los mismos valores de ‘geometry’:
geometry_1 = map_municipios[map_municipios['municipio'] == 'Manzanares el Real']['geometry']
geometry_2 = map_municipios[map_municipios['municipio'] == 'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)']['geometry']
print(geometry_1)
print(geometry_2)
12 MULTIPOLYGON (((-3.90600 40.68217, -3.90882 40...
Name: geometry, dtype: geometry
74 MULTIPOLYGON (((-3.92431 40.67975, -3.91307 40...
Name: geometry, dtype: geometry
geometry_3 = map_municipios[map_municipios['municipio'] == 'Navacerrada']['geometry']
geometry_4 = map_municipios[map_municipios['municipio'] == 'Jurisdicción Mancomunada de Cerdedilla y Navacerrada']['geometry']
print(geometry_3)
print(geometry_4)
171 MULTIPOLYGON (((-3.96854 40.76714, -3.97736 40...
Name: geometry, dtype: geometry
109 MULTIPOLYGON (((-4.00244 40.78843, -3.99731 40...
Name: geometry, dtype: geometry
No tienen el mismo valor de ‘geometry’, por lo tanto solo vamos a corregir ‘Horcajo de la Sierra’ y los otros dos desaparecerán a la hora de mergear con nuestro dataframe:
map_municipios['municipio'] = map_municipios['municipio'].replace({
'Horcajo de la Sierra': 'Horcajo de la Sierra-Aoslos',
})
no_intersection_map = set(map_municipios['municipio']).difference(set(df['municipio']))
len(no_intersection_map)
no_intersection_map
{'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
'Jurisdicción Mancomunada de Cerdedilla y Navacerrada'}
Ya podemos mergear para que nuestro dataframe tenga información geoespacial:
df_2019 = pd.merge(df, map_municipios, how='left', on='municipio')
df_2019.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 42 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 municipio 179 non-null object
1 link 179 non-null object
2 pp 179 non-null object
3 pp_porcentaje 179 non-null object
4 cs 179 non-null object
5 cs_porcentaje 179 non-null object
6 psoe 179 non-null object
7 psoe_porcentaje 179 non-null object
8 mas_madrid 178 non-null object
9 mas_madrid_porcentaje 178 non-null object
10 podemos_iu 179 non-null object
11 podemos_iu_porcentaje 179 non-null object
12 vox 179 non-null object
13 vox_porcentaje 179 non-null object
14 pacma 159 non-null object
15 pacma_porcentaje 159 non-null object
16 fe_de_las_jons 134 non-null object
17 fe_de_las_jons_porcentaje 134 non-null object
18 pum+j 120 non-null object
19 pum+j_porcentaje 120 non-null object
20 pcte 113 non-null object
21 pcte_porcentaje 113 non-null object
22 upyd 120 non-null object
23 upyd_porcentaje 120 non-null object
24 ph 96 non-null object
25 ph_porcentaje 96 non-null object
26 pcas_tc 95 non-null object
27 pcas_tc_porcentaje 95 non-null object
28 p_lib 90 non-null object
29 p_lib_porcentaje 90 non-null object
30 uleg 71 non-null object
31 uleg_porcentaje 71 non-null object
32 escrutado 179 non-null object
33 votos_totales 179 non-null object
34 votos_totales_porcentaje 179 non-null object
35 abstencion 179 non-null object
36 abstencion_porcentaje 179 non-null object
37 votos_nulos 179 non-null object
38 votos_nulos_porcentaje 179 non-null object
39 votos_blancos 179 non-null object
40 votos_blancos_porcentaje 179 non-null object
41 geometry 179 non-null geometry
dtypes: geometry(1), object(41)
memory usage: 60.1+ KB
Pasamos las columnas a valores numéricos, excepto ‘municipio’, ‘link’ y ‘geometry’:
cols = df_2019.columns.drop(['municipio', 'link', 'geometry'])
df_2019[cols] = df_2019[cols].apply(pd.to_numeric, errors='coerce', axis=1)
df_2019.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 42 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 municipio 179 non-null object
1 link 179 non-null object
2 pp 179 non-null float64
3 pp_porcentaje 179 non-null float64
4 cs 179 non-null float64
5 cs_porcentaje 179 non-null float64
6 psoe 179 non-null float64
7 psoe_porcentaje 179 non-null float64
8 mas_madrid 178 non-null float64
9 mas_madrid_porcentaje 178 non-null float64
10 podemos_iu 179 non-null float64
11 podemos_iu_porcentaje 179 non-null float64
12 vox 179 non-null float64
13 vox_porcentaje 179 non-null float64
14 pacma 159 non-null float64
15 pacma_porcentaje 159 non-null float64
16 fe_de_las_jons 134 non-null float64
17 fe_de_las_jons_porcentaje 134 non-null float64
18 pum+j 120 non-null float64
19 pum+j_porcentaje 120 non-null float64
20 pcte 113 non-null float64
21 pcte_porcentaje 113 non-null float64
22 upyd 120 non-null float64
23 upyd_porcentaje 120 non-null float64
24 ph 96 non-null float64
25 ph_porcentaje 96 non-null float64
26 pcas_tc 95 non-null float64
27 pcas_tc_porcentaje 95 non-null float64
28 p_lib 90 non-null float64
29 p_lib_porcentaje 90 non-null float64
30 uleg 71 non-null float64
31 uleg_porcentaje 70 non-null float64
32 escrutado 179 non-null float64
33 votos_totales 179 non-null float64
34 votos_totales_porcentaje 179 non-null float64
35 abstencion 179 non-null float64
36 abstencion_porcentaje 179 non-null float64
37 votos_nulos 179 non-null float64
38 votos_nulos_porcentaje 179 non-null float64
39 votos_blancos 179 non-null float64
40 votos_blancos_porcentaje 179 non-null float64
41 geometry 179 non-null geometry
dtypes: float64(39), geometry(1), object(2)
memory usage: 60.1+ KB
1.1.-Resumen del procedimiento:¶
Todo el proceso hasta obtener una primera versión del dataframe se puede resumir en las siguientes funciones:
def table_escrutado(link):
from bs4 import BeautifulSoup
import requests
html_text_municipio = requests.get(link).text
soup = BeautifulSoup(html_text_municipio, 'lxml')
table_escrutado = soup.find('table', {'id': 'tablaResumen'})
table_escrutado_trs = table_escrutado.find_all('tr')
results_table_escrutado = []
for tr in table_escrutado_trs:
local_table_escrutado = {}
local_table_escrutado['encabezado'] = None if tr.select_one('.encabezado') is None else tr.select_one('.encabezado').text
local_table_escrutado['numero'] = None if tr.select_one('.tipoNumero') is None else tr.select_one('.tipoNumero').text
local_table_escrutado['porcentaje'] = None if tr.select_one('.tipoPorciento') is None else tr.select_one('.tipoPorciento').text
results_table_escrutado.append(local_table_escrutado)
return results_table_escrutado
def table_partido(link):
from bs4 import BeautifulSoup
import requests
html_text_municipio = requests.get(link).text
soup = BeautifulSoup(html_text_municipio, 'lxml')
table_partido = soup.find('table', {'id': 'tablaVotosPartidos'})
table_partido_trs = table_partido.find_all('tr')
results_table_partido = []
for tr in table_partido_trs:
local_table_partido = {}
local_table_partido['partido'] = None if tr.select_one('.nombrePartido') is None else tr.select_one('.nombrePartido').text
local_table_partido['numero_votos'] = None if tr.select_one('.tipoNumeroVotos') is None else tr.select_one('.tipoNumeroVotos').text
local_table_partido['porcentaje'] = None if tr.select_one('.tipoPorcientoVotos') is None else tr.select_one('.tipoPorcientoVotos').text
results_table_partido.append(local_table_partido)
return results_table_partido
def clean_strings_and_turn_float(value):
if ' %' in value:
return value.replace(' %', '').replace(',', '.')
else:
return value.replace('.', '')
def result_resume(results_table_escrutado):
results_resume = []
for result in results_table_escrutado:
local_resume = {}
if result.get('encabezado') == 'Escrutado:':
local_resume['escrutado'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Votos contabilizados:':
local_resume['votos_totales'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['votos_totales_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Abstenciones:':
local_resume['abstencion'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['abstencion_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Votos nulos:':
local_resume['votos_nulos'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['votos_nulos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
if result.get('encabezado') == 'Votos en blanco:':
local_resume['votos_blancos'] = clean_strings_and_turn_float(result.get('numero'))
local_resume['votos_blancos_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
results_resume.append(local_resume)
return results_resume
def strip_accents(text):
import unicodedata
try:
text = unicode(text, 'utf-8')
except NameError: # unicode is a default on python 3
pass
text = unicodedata.normalize('NFD', text)\
.encode('ascii', 'ignore')\
.decode("utf-8")
return str(text)
def result_partido_resume(results_table_partido):
results_resume_partido = []
for result in results_table_partido:
local_resume = {}
if result.get('partido') == None:
continue
else:
partido = strip_accents(result.get('partido').lower().replace('-', '_').replace(' ', '_'))
local_resume[partido] = clean_strings_and_turn_float(result.get('numero_votos'))
local_resume[partido+'_porcentaje'] = clean_strings_and_turn_float(result.get('porcentaje'))
results_resume_partido.append(local_resume)
return results_resume_partido
def prepare_data_from_web(url, lis):
data_from_web = []
for li in lis:
for link in li.find_all('a'):
local_result = {}
local_result['municipio'] = link.text
local_result['link'] = url+link.get('href')
local_result['escrutinio'] = table_escrutado(local_result['link'])
local_result['partidos'] = table_partido(local_result['link'])
data_from_web.append(local_result)
return data_from_web
def format_data(data_from_web):
data_formatted = []
for data in data_from_web:
local_result = {}
local_result['municipio'] = data['municipio']
local_result['link'] = data['link']
local_result['escrutinio'] = result_resume(data['escrutinio'])
local_result['partidos'] = result_partido_resume(data['partidos'])
data_formatted.append(local_result)
return data_formatted
def data_frame_preparation(data_formatted):
import pandas as pd
## dataframe preparation
df_partidos = pd.json_normalize(data_formatted, record_path='partidos', meta=['municipio', 'link'])
df_partidos = df_partidos.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)
df_escrutinio = pd.json_normalize(data_formatted, record_path='escrutinio', meta=['municipio'])
df_escrutinio = df_escrutinio.groupby('municipio').apply(lambda x: x.bfill().head(1)).reset_index(drop=True)
df = pd.merge(df_partidos, df_escrutinio, on='municipio', how='outer')
## columns position switched:
first_column = df.pop('municipio')
second_column = df.pop('link')
df.insert(0, 'municipio', first_column)
df.insert(1, 'link', second_column)
return df
def extract_data_from_web(url):
from bs4 import BeautifulSoup
import requests
# html code processing from url:
html_text = requests.get(url).text
soup = BeautifulSoup(html_text, 'lxml')
ul = soup.select('ul.estirar')[1]
lis = ul.find_all('li')
# preparing data before formatting:
data_from_web = prepare_data_from_web(url, lis)
# data formatted:
data_formatted = format_data(data_from_web)
## dataframe
df = data_frame_preparation(data_formatted)
return df
1.2.-Preparación de los datos de 2021:¶
Aplicamos la función que resumen el procedimiento:
url = 'https://resultados.elpais.com/elecciones/2021/autonomicas/12/'
df_2021 = extract_data_from_web(url)
df_2021
| municipio | link | pp | pp_porcentaje | vox | vox_porcentaje | mas_madrid | mas_madrid_porcentaje | psoe | psoe_porcentaje | ... | pole_porcentaje | escrutado | votos_totales | votos_totales_porcentaje | abstencion | abstencion_porcentaje | votos_nulos | votos_nulos_porcentaje | votos_blancos | votos_blancos_porcentaje | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Ajalvir | https://resultados.elpais.com/elecciones/2021/... | 1148 | 49.4 | 340 | 14.63 | 330 | 14.2 | 325 | 13.98 | ... | NaN | 100 | 2339 | 71.95 | 912 | 28.05 | 15 | 0.64 | 10 | 0.43 |
| 1 | Alameda del Valle | https://resultados.elpais.com/elecciones/2021/... | 64 | 38.1 | 16 | 9.52 | 36 | 21.43 | 30 | 17.86 | ... | NaN | 100 | 168 | 86.15 | 27 | 13.85 | 0 | 0 | 3 | 1.79 |
| 2 | Alcalá de Henares | https://resultados.elpais.com/elecciones/2021/... | 42645 | 42.58 | 9735 | 9.72 | 15540 | 15.52 | 19926 | 19.9 | ... | 0.01 | 99.54 | 100932 | 74.12 | 35235 | 25.88 | 789 | 0.78 | 603 | 0.6 |
| 3 | Alcobendas | https://resultados.elpais.com/elecciones/2021/... | 30588 | 49.6 | 5533 | 8.97 | 8212 | 13.32 | 10757 | 17.44 | ... | 0.01 | 100 | 62036 | 77.16 | 18367 | 22.84 | 363 | 0.59 | 297 | 0.48 |
| 4 | Alcorcón | https://resultados.elpais.com/elecciones/2021/... | 39588 | 40.9 | 7841 | 8.1 | 16945 | 17.51 | 19798 | 20.45 | ... | 0.01 | 100 | 97563 | 76.87 | 29356 | 23.13 | 769 | 0.79 | 565 | 0.58 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 174 | Villar del Olmo | https://resultados.elpais.com/elecciones/2021/... | 558 | 48.27 | 163 | 14.1 | 149 | 12.89 | 154 | 13.32 | ... | 0.09 | 100 | 1165 | 74.54 | 398 | 25.46 | 9 | 0.77 | 7 | 0.61 |
| 175 | Villarejo de Salvanés | https://resultados.elpais.com/elecciones/2021/... | 1714 | 41.66 | 420 | 10.21 | 515 | 12.52 | 1031 | 25.06 | ... | 0.05 | 100 | 4150 | 76.01 | 1310 | 23.99 | 36 | 0.87 | 24 | 0.58 |
| 176 | Villaviciosa de Odón | https://resultados.elpais.com/elecciones/2021/... | 9769 | 57.3 | 1900 | 11.14 | 1790 | 10.5 | 2104 | 12.34 | ... | NaN | 100 | 17125 | 82.34 | 3674 | 17.66 | 75 | 0.44 | 55 | 0.32 |
| 177 | Villavieja del Lozoya | https://resultados.elpais.com/elecciones/2021/... | 73 | 39.89 | 16 | 8.74 | 42 | 22.95 | 28 | 15.3 | ... | NaN | 100 | 183 | 80.26 | 45 | 19.74 | 0 | 0 | 1 | 0.55 |
| 178 | Zarzalejo | https://resultados.elpais.com/elecciones/2021/... | 315 | 36.21 | 61 | 7.01 | 179 | 20.57 | 148 | 17.01 | ... | NaN | 100 | 879 | 72.11 | 340 | 27.89 | 9 | 1.02 | 2 | 0.23 |
179 rows × 51 columns
len(df_2021)
179
Se ve que el número de municipios coincide con los que tiene la CAM; aún así vamos a comprobar antes de añadir la información geoespacial.
1.2.1-Añadir información geoespacial al dataframe:¶
Vemos si pueden haber diferencias entre los nombres de los municipios:
no_intersection_map = set(map_municipios['municipio']).difference(set(df_2021['municipio']))
len(no_intersection_map)
no_intersection_map
{'Jurisdicción Macomunada de El Boalo y Manzanares el Real (El Chaparral)',
'Jurisdicción Mancomunada de Cerdedilla y Navacerrada'}
Son los mismos que antes, procedemos a mergear los dataframes:
import pandas as pd
df_2021 = pd.merge(df_2021, map_municipios, how='left', on='municipio')
df_2021.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 52 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 municipio 179 non-null object
1 link 179 non-null object
2 pp 179 non-null object
3 pp_porcentaje 179 non-null object
4 vox 179 non-null object
5 vox_porcentaje 179 non-null object
6 mas_madrid 179 non-null object
7 mas_madrid_porcentaje 179 non-null object
8 psoe 179 non-null object
9 psoe_porcentaje 179 non-null object
10 podemos_iu 178 non-null object
11 podemos_iu_porcentaje 178 non-null object
12 cs 174 non-null object
13 cs_porcentaje 174 non-null object
14 pacma 153 non-null object
15 pacma_porcentaje 153 non-null object
16 fe_de_las_jons 103 non-null object
17 fe_de_las_jons_porcentaje 103 non-null object
18 pcte 96 non-null object
19 pcte_porcentaje 96 non-null object
20 ph 75 non-null object
21 ph_porcentaje 75 non-null object
22 3e_en_accion 100 non-null object
23 3e_en_accion_porcentaje 100 non-null object
24 eb 103 non-null object
25 eb_porcentaje 103 non-null object
26 partido_autonomos 110 non-null object
27 partido_autonomos_porcentaje 110 non-null object
28 p_lib 94 non-null object
29 p_lib_porcentaje 94 non-null object
30 udec 93 non-null object
31 udec_porcentaje 93 non-null object
32 pum+j 114 non-null object
33 pum+j_porcentaje 114 non-null object
34 volt 109 non-null object
35 volt_porcentaje 109 non-null object
36 recortes_cero_pcas_tc_gv_m 93 non-null object
37 recortes_cero_pcas_tc_gv_m_porcentaje 93 non-null object
38 pcoe_pcpe 83 non-null object
39 pcoe_pcpe_porcentaje 83 non-null object
40 pole 56 non-null object
41 pole_porcentaje 56 non-null object
42 escrutado 179 non-null object
43 votos_totales 179 non-null object
44 votos_totales_porcentaje 179 non-null object
45 abstencion 179 non-null object
46 abstencion_porcentaje 179 non-null object
47 votos_nulos 179 non-null object
48 votos_nulos_porcentaje 179 non-null object
49 votos_blancos 179 non-null object
50 votos_blancos_porcentaje 179 non-null object
51 geometry 179 non-null geometry
dtypes: geometry(1), object(51)
memory usage: 74.1+ KB
# pasamos a valores numéricos todas las columnas excepto 'municipio', 'link' y 'geometry':
cols = df_2021.columns.drop(['municipio', 'link', 'geometry'])
df_2021[cols] = df_2021[cols].apply(pd.to_numeric, errors='coerce', axis=1)
df_2021.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 52 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 municipio 179 non-null object
1 link 179 non-null object
2 pp 179 non-null float64
3 pp_porcentaje 179 non-null float64
4 vox 179 non-null float64
5 vox_porcentaje 179 non-null float64
6 mas_madrid 179 non-null float64
7 mas_madrid_porcentaje 179 non-null float64
8 psoe 179 non-null float64
9 psoe_porcentaje 179 non-null float64
10 podemos_iu 178 non-null float64
11 podemos_iu_porcentaje 178 non-null float64
12 cs 174 non-null float64
13 cs_porcentaje 174 non-null float64
14 pacma 153 non-null float64
15 pacma_porcentaje 153 non-null float64
16 fe_de_las_jons 103 non-null float64
17 fe_de_las_jons_porcentaje 103 non-null float64
18 pcte 96 non-null float64
19 pcte_porcentaje 96 non-null float64
20 ph 75 non-null float64
21 ph_porcentaje 75 non-null float64
22 3e_en_accion 100 non-null float64
23 3e_en_accion_porcentaje 100 non-null float64
24 eb 103 non-null float64
25 eb_porcentaje 103 non-null float64
26 partido_autonomos 110 non-null float64
27 partido_autonomos_porcentaje 110 non-null float64
28 p_lib 94 non-null float64
29 p_lib_porcentaje 94 non-null float64
30 udec 93 non-null float64
31 udec_porcentaje 93 non-null float64
32 pum+j 114 non-null float64
33 pum+j_porcentaje 114 non-null float64
34 volt 109 non-null float64
35 volt_porcentaje 109 non-null float64
36 recortes_cero_pcas_tc_gv_m 93 non-null float64
37 recortes_cero_pcas_tc_gv_m_porcentaje 93 non-null float64
38 pcoe_pcpe 83 non-null float64
39 pcoe_pcpe_porcentaje 83 non-null float64
40 pole 56 non-null float64
41 pole_porcentaje 56 non-null float64
42 escrutado 179 non-null float64
43 votos_totales 179 non-null float64
44 votos_totales_porcentaje 179 non-null float64
45 abstencion 179 non-null float64
46 abstencion_porcentaje 179 non-null float64
47 votos_nulos 179 non-null float64
48 votos_nulos_porcentaje 179 non-null float64
49 votos_blancos 179 non-null float64
50 votos_blancos_porcentaje 179 non-null float64
51 geometry 179 non-null geometry
dtypes: float64(49), geometry(1), object(2)
memory usage: 74.1+ KB
2.-Contexto y análisis de los resultados¶
El pasado 4 de mayo de 2021 se celebraron elecciones autonómicas en la Comunidad de Madrid, donde el Partido Popular fue el partido más votado con Isabel Díaz Ayuso repitiendo como candidata.
Estas elecciones se celebraron en un contexto particular: la irrupción del COVID-19 en España y el fin del período de confinamiento estricto de 99 días.
Las últimas elecciones se celebraron en 2019 donde el PSOE encabezado por Ángel Gabilondo fue el partido más votado frente a un PP desgastado a nivel regional y nacional por los casos de corrupción que tenían como epicentro la Comunidad de Madrid, pero no pudo formar gobierno por el apoyo de Ciudadanos (Cs) a Isabel Díaz Ayuso a cambio de formar un gobierno autonómico de coalición. Similares gobiernos PP-Cs existían en otras regiones como Castilla y León o Murcia.
Precisamente en Murcia, debido a los rumores de negociaciones entre PSOE y Cs para retirar el apoyo al PP, llevaron a Isabel Díaz Ayuso a anticiparse y convocar elecciones para no perder el gobierno autonómico sabiendo que las encuestas le eran favorables por dos aspectos:
La estrategia de oposición al Gobierno nacional por la gestión de la COVID-19 y canalizar el malestar que generó en muchos sectores el confinamiento.
Las encuestas que indicaban una total caída del apoyo electoral a Cs, su socio de gobierno.
Vamos a pasar a analizar los resultados y a comentarlos.
2.1.-Exposición de los resultados¶
En las elecciones autonómicas de 2019 los partidos que obtuvieron representación fueron PSOE, PP, Cs, Más Madrid, Vox y Podemos-IU. Por otro lado, en las elecciones de 2021 vemos el siguiente resultado: PP, Más Madrid, PSOE, Vox, Podemos-IU, desapareciendo Cs del mapa electoral.
Hay que indicar que para obtener representación institucional en las elecciones autonómicas hay que superar la barrera del 5% de votos. En las elecciones de 2019 se repartían 132 escaños, mientras que en 2021 por el aumento poblacional el reparto fue de 136 escaños.
Vamos a pasar a exponer los resultados absolutos por partidos tomando como referencia los resultados de cada unas de las elecciones.
import numpy as np
import matplotlib.pyplot as plt
from chart_studio import plotly
import plotly.graph_objs as go
from plotly import tools
from plotly.offline import iplot, init_notebook_mode
init_notebook_mode()
import plotly.graph_objects as go
parties=['pp', 'psoe', 'cs', 'mas_madrid', 'vox', 'podemos_iu']
final_results_2019 = [
df_2019['pp'].sum(),
df_2019['psoe'].sum(),
df_2019['cs'].sum(),
df_2019['mas_madrid'].sum(),
df_2019['vox'].sum(),
df_2019['podemos_iu'].sum(),
]
final_results_2021 = [
df_2021['pp'].sum(),
df_2021['psoe'].sum(),
df_2021['cs'].sum(),
df_2021['mas_madrid'].sum(),
df_2021['vox'].sum(),
df_2021['podemos_iu'].sum(),
]
fig = go.Figure(data=[
go.Bar(name='2021', x=parties, y=final_results_2021),
go.Bar(name='2019', x=parties, y=final_results_2019)
])
# Change the bar mode
fig.update_layout(title='Distribución del voto por partidos', barmode='group')
fig.show()
Para saber cuál es la variación de los porcentajes de cada partido vamos a calcularlos con respecto al total de votos de cada elección. Para ello extraemos los resultados totales de cada partido en cada elección y creamos una función que nos da la diferencia en votos y porcentajes totales.
# Dataset 2019
# excluimos las columnas de 'municipio', 'link' y 'geometry':
cols_2019 = df_2019.columns.drop(['municipio', 'link', 'geometry'])
# excluimos los porcentajes:
cols_votes_2019 = [col for col in cols_2019 if '_porcentaje' not in col]
cols_votes_2019 = [col for col in cols_votes_2019 if '_share' not in col]
# Dataset 2021
# excluimos las columnas de 'municipio', 'link' y 'geometry':
cols_2021 = df_2021.columns.drop(['municipio', 'link', 'geometry'])
# excluimos los porcentajes:
cols_votes_2021 = [col for col in cols_2021 if '_porcentaje' not in col]
cols_votes_2021 = [col for col in cols_votes_2019 if '_share' not in col]
def total_results(party, df_year):
total_votes = df_year[party].sum()
total_percentage = (df_year[party].sum()/(df_year['votos_totales'].sum()))*100
total_votes = str("{:,}".format(total_votes)).strip('.0')+' votos'
total_percentage = str(round(total_percentage, 2))+' %'
return [total_votes, total_percentage]
def difference_elections(party, df_year_1, df_year_2):
votes_difference = df_year_1[party].sum() - df_year_2[party].sum()
percentage_1 = (df_year_1[party].sum()/(df_year_1['votos_totales'].sum()))*100
percentage_2 = (df_year_2[party].sum()/(df_year_2['votos_totales'].sum()))*100
percentage_difference = percentage_1 - percentage_2
votes_difference = str("{:,}".format(votes_difference)).strip('.0')+' votos'
percentage_difference = str(round(percentage_difference, 2))+' %'
return [votes_difference, percentage_difference]
Partido Popular¶
print('PP_2019: '+ total_results('pp', df_2019)[0] +' / '+ total_results('pp', df_2019)[1])
print('PP_2021: '+ total_results('pp', df_2021)[0] +' / '+ total_results('pp', df_2021)[1])
print('PP_difference: '+ difference_elections('pp', df_2021, df_2019)[0] +' / '+ difference_elections('pp', df_2021, df_2019)[1])
PP_2019: 717,044 votos / 22.17 %
PP_2021: 1,620,213 votos / 44.46 %
PP_difference: 903,169 votos / 22.29 %
PSOE¶
print('PSOE_2019: '+ total_results('psoe', df_2019)[0] +' / '+ total_results('psoe', df_2019)[1])
print('PSOE_2021: '+ total_results('psoe', df_2021)[0] +' / '+ total_results('psoe', df_2021)[1])
print('PSOE_difference: '+ difference_elections('psoe', df_2021, df_2019)[0] +' / '+ difference_elections('psoe', df_2021, df_2019)[1])
PSOE_2019: 880,969 votos / 27.23 %
PSOE_2021: 610,19 votos / 16.74 %
PSOE_difference: -270,779 votos / -10.49 %
Ciudadanos¶
print('CS_2019: '+ total_results('cs', df_2019)[0] +' / '+ total_results('cs', df_2019)[1])
print('CS_2021: '+ total_results('cs', df_2021)[0] +' / '+ total_results('cs', df_2021)[1])
print('CS_difference: '+ difference_elections('cs', df_2021, df_2019)[0] +' / '+ difference_elections('cs', df_2021, df_2019)[1])
CS_2019: 626,766 votos / 19.37 %
CS_2021: 129,216 votos / 3.55 %
CS_difference: -497,55 votos / -15.83 %
Más Madrid¶
print('MAS_MADRID_2019: '+ total_results('mas_madrid', df_2019)[0] +' / '+ total_results('mas_madrid', df_2019)[1])
print('MAS_MADRID_2021: '+ total_results('mas_madrid', df_2021)[0] +' / '+ total_results('mas_madrid', df_2021)[1])
print('MAS_MADRID_difference: '+ difference_elections('mas_madrid', df_2021, df_2019)[0] +' / '+ difference_elections('mas_madrid', df_2021, df_2019)[1])
MAS_MADRID_2019: 472,221 votos / 14.6 %
MAS_MADRID_2021: 614,66 votos / 16.87 %
MAS_MADRID_difference: 142,439 votos / 2.27 %
PODEMOS-IU¶
print('PODEMOS_IU_2019: '+ total_results('podemos_iu', df_2019)[0] +' / '+ total_results('podemos_iu', df_2019)[1])
print('PODEMOS_IU_2021: '+ total_results('podemos_iu', df_2021)[0] +' / '+ total_results('podemos_iu', df_2021)[1])
print('PODEMOS_IU_difference: '+ difference_elections('podemos_iu', df_2021, df_2019)[0] +' / '+ difference_elections('podemos_iu', df_2021, df_2019)[1])
PODEMOS_IU_2019: 179,958 votos / 5.56 %
PODEMOS_IU_2021: 261,01 votos / 7.16 %
PODEMOS_IU_difference: 81,052 votos / 1.6 %
Vox¶
print('VOX_2019: '+ total_results('vox', df_2019)[0] +' / '+ total_results('vox', df_2019)[1])
print('VOX_2021: '+ total_results('vox', df_2021)[0] +' / '+ total_results('vox', df_2021)[1])
print('VOX_difference: '+ difference_elections('vox', df_2021, df_2019)[0] +' / '+ difference_elections('vox', df_2021, df_2019)[1])
VOX_2019: 285,836 votos / 8.84 %
VOX_2021: 330,66 votos / 9.07 %
VOX_difference: 44,824 votos / 0.24 %
Vemos claramente cómo el PP es el partido que más crece en número de votos (+910,607 votos / 22.33%), frente a Cs que es el que más disminuye su apoyo electoral (-501,023 votos / -15.84%) pasando de 631,117 votos (19.39%) a 130,094 votos (3.55%), lo que implica no llegar al 5% mínimo para obtener representación parlamentaria. Otro partido que pierde apoyos es el PSOE (-272,236 votos / -10.49%).
El resto de partidos aumenta sus apoyos: Más Madrid pasa de 474,725 votos (14.59%) a 618,285 votos (16.85%), lo que supone un incremento de 142,439 votos (2.27%), consiguiendo dar el sorpaso al PSOE dentro del bloque de la izquierda; le sigue Podemos-IU de 181,242 votos (5.57%) a 262,45 votos (7.15%), lo que supone un crecimiento en 81,208 votos (1.59%). Por último, Vox es el partido de los que obtiene representación que menos crece: 288,313 votos (8.86%) en 2019 a 333,447 votos (9.09%) en 2021, lo que supone un aumento de 44,824 votos (0.24%).
2.2.-Análisis de la distribución del voto¶
A continuación vamos a hacer un análisis de la distribución del voto y cómo ha ido variando entre estas dos elecciones.
Vamos a analizar el voto conjunto de PP y PSOE, partidos que desde la Transición se han ido repartiendo a nivel nacional, autonómico y provincial la mayoría de los votos en cada elección. Esto nos permitirá ver qué apoyo representan frente a los partidos que han surgido en la última década (Podemos, Cs, Vox y Más Madrid) o que planeaban desde la Transición un nuevo modelo constitucional frente al consenso de PP y PSOE, como es el caso de IU.
Por último analizaremos la distribución del voto en torno a los ejes derecha (PP, Cs, Vox) e izquierda (PSOE, Más Madrid, Podemos-IU). Vamos a escoger esta división porque es la que siempre se ha resaltado desde los medios de comunicación y confirmado muchas veces por la política de alianzas entre los diferentes partidos, aunque hay que indicar que Cs varias veces ha prestado apoyo al PSOE frente al PP y que hay muchos analistas que indican que las políticas económicas y sociales del PSOE no se deberían considerar dentro del eje de izquierdas.
PP-PSOE (vs) Más Madrid-Cs-Vox-Podemos-IU
# 2019:
pp_2019 = df_2019['pp'].sum()
psoe_2019 = df_2019['psoe'].sum()
cs_2019 = df_2019['cs'].sum()
mas_madrid_2019 = df_2019['mas_madrid'].sum()
podemos_iu_2019 = df_2019['podemos_iu'].sum()
vox_2019 = df_2019['podemos_iu'].sum()
pp_psoe_2019 = pp_2019 + psoe_2019
otros_partidos_2019 = cs_2019 + mas_madrid_2019 + podemos_iu_2019 + vox_2019
pp_psoe_percentage_2019 = (pp_psoe_2019/df_2019['votos_totales'].sum())*100
otros_partidos_percentage_2019 = (otros_partidos_2019/df_2019['votos_totales'].sum())*100
# 2021:
pp_2021 = df_2021['pp'].sum()
psoe_2021 = df_2021['psoe'].sum()
cs_2021 = df_2021['cs'].sum()
mas_madrid_2021 = df_2021['mas_madrid'].sum()
podemos_iu_2021 = df_2021['podemos_iu'].sum()
vox_2021 = df_2021['podemos_iu'].sum()
pp_psoe_2021 = pp_2021 + psoe_2021
otros_partidos_2021 = cs_2021 + mas_madrid_2021 + podemos_iu_2021 + vox_2021
pp_psoe_percentage_2021 = (pp_psoe_2021/df_2021['votos_totales'].sum())*100
otros_partidos_percentage_2021 = (otros_partidos_2021/df_2021['votos_totales'].sum())*100
# Diferencia entre 2019 y 2021
## diferencia de votos:
pp_psoe_vote_diff = pp_psoe_2021 - pp_psoe_2019
otros_partidos_vote_diff = otros_partidos_2021 - otros_partidos_2019
## diferencia de porcentajes:
pp_psoe_per_diff = pp_psoe_percentage_2021 - pp_psoe_percentage_2019
otros_per_diff = otros_partidos_percentage_2021 - otros_partidos_percentage_2019
import plotly.graph_objects as go
parties=['pp_psoe', 'otros_partidos']
final_results_2019 = [
pp_psoe_2019,
otros_partidos_2019,
]
final_results_2021 = [
pp_psoe_2021,
otros_partidos_2021,
]
fig = go.Figure(data=[
go.Bar(name='2021', x=parties, y=final_results_2021),
go.Bar(name='2019', x=parties, y=final_results_2019)
])
# Change the bar mode
fig.update_layout(title='PP-PSOE (vs) Más Madrid-Cs-Vox-Podemos-IU', barmode='group')
fig.show()
# 2019
pp_psoe_2019 = str("{:,}".format(pp_psoe_2019)).strip('.0')+' votos'
pp_psoe_percentage_2019 = str(round(pp_psoe_percentage_2019, 2))+' %'
otros_partidos_2019 = str("{:,}".format(otros_partidos_2019)).strip('.0')+' votos'
otros_partidos_percentage_2019 = str(round(otros_partidos_percentage_2019, 2))+' %'
# 2021
pp_psoe_2021 = str("{:,}".format(pp_psoe_2021)).strip('.0')+' votos'
pp_psoe_percentage_2021 = str(round(pp_psoe_percentage_2021, 2))+' %'
otros_partidos_2021 = str("{:,}".format(otros_partidos_2021)).strip('.0')+' votos'
otros_partidos_percentage_2021 = str(round(otros_partidos_percentage_2021, 2))+' %'
# Diferencia entre 2019 y 2021
## diferencia de votos:
pp_psoe_vote_diff = str("{:,}".format(pp_psoe_vote_diff)).strip('.0')+' votos'
otros_partidos_vote_diff = str("{:,}".format(otros_partidos_vote_diff)).strip('.0')+' votos'
## diferencia de porcentajes:
pp_psoe_per_diff = str(round(pp_psoe_per_diff, 2))+' %'
otros_per_diff = str(round(otros_per_diff, 2))+' %'
print('PP_PSOE_2019: '+ pp_psoe_2019 +' / '+ pp_psoe_percentage_2019)
print('PP_PSOE_2021: '+ pp_psoe_2021 +' / '+ pp_psoe_percentage_2021)
print('PP_PSOE_DIFF: '+ pp_psoe_vote_diff +' / '+ pp_psoe_per_diff)
PP_PSOE_2019: 1,598,013 votos / 49.4 %
PP_PSOE_2021: 2,230,403 votos / 61.2 %
PP_PSOE_DIFF: 632,39 votos / 11.8 %
print('OTROS_PARTIDOS_2019: '+ otros_partidos_2019 +' / '+ otros_partidos_percentage_2019)
print('OTROS_PARTIDOS_2021: '+ otros_partidos_2021 +' / '+ otros_partidos_percentage_2021)
print('OTROS_PARTIDOS_DIFF: '+ otros_partidos_vote_diff +' / '+ otros_per_diff)
OTROS_PARTIDOS_2019: 1,458,903 votos / 45.1 %
OTROS_PARTIDOS_2021: 1,265,896 votos / 34.73 %
OTROS_PARTIDOS_DIFF: -193,007 votos / -10.36 %
Podemos ver que de las elecciones de 2019 a las de 2021 se produce una reconfiguración de los partidos tradicionales del Régimen del 78 (PP-PSOE) al pasar de 1,606,519 votos (49.36%) en 2019 a 2,244,890 votos (61.2%) en 2021, lo que supone un aumento de 638,371 votos (11.84%) liderado por el espectacular aumento del PP.
Por otro lado, los partidos al margen de PP-PSOE pasan de acumular 1,468,326 votos (45.11%) en 2019 a 1,273,279 votos (34.71%) en 2021, lo que supone una disminución de 195,047 votos (-10.4%). El principal responsable de esta caída está en el desplome de Cs, ya que como vimos Más Madrid, Podemos-IU y Vox crecieron en apoyos.
Se podría decir que bastante del apoyo que obtuvo Cs en 2019 pasó al PP en 2021 y que parte del apoyo del PSOE pudo haberse repartido tanto hacia la izquierda (principalmente Más Madrid) como hacia la derecha (principalmente PP).
Derecha(PP-Cs-Vox) (vs) Izquierda(PSOE, Más Madrid, Podemos-IU)
# Partidos
## 2019
pp_2019 = df_2019['pp'].sum()
psoe_2019 = df_2019['psoe'].sum()
cs_2019 = df_2019['cs'].sum()
mas_madrid_2019 = df_2019['mas_madrid'].sum()
podemos_iu_2019 = df_2019['podemos_iu'].sum()
vox_2019 = df_2019['podemos_iu'].sum()
## 2021
pp_2021 = df_2021['pp'].sum()
psoe_2021 = df_2021['psoe'].sum()
cs_2021 = df_2021['cs'].sum()
mas_madrid_2021 = df_2021['mas_madrid'].sum()
podemos_iu_2021 = df_2021['podemos_iu'].sum()
vox_2021 = df_2021['podemos_iu'].sum()
# 2019:
## Derecha:
right_votes_2019 = pp_2019 + cs_2019 + vox_2019
right_percentage_2019 = (right_votes_2019/df_2019['votos_totales'].sum())*100
## Izquierda:
left_votes_2019 = psoe_2019 + mas_madrid_2019 + podemos_iu_2019
left_percentage_2019 = (left_votes_2019/df_2019['votos_totales'].sum())*100
## Derecha-Izquierda diferencia:
right_left_vote_diff_2019 = right_votes_2019 - left_votes_2019
right_left_perc_diff_2019 = right_percentage_2019 - left_percentage_2019
# 2021:
## Derecha:
right_votes_2021 = pp_2021 + cs_2021 + vox_2021
right_percentage_2021 = (right_votes_2021/df_2021['votos_totales'].sum())*100
## Izquierda:
left_votes_2021 = psoe_2021 + mas_madrid_2021 + podemos_iu_2021
left_percentage_2021 = (left_votes_2021/df_2021['votos_totales'].sum())*100
## Derecha-Izquierda diferencia:
right_left_vote_diff_2021 = right_votes_2021 - left_votes_2021
right_left_perc_diff_2021 = right_percentage_2021 - left_percentage_2021
# Diferencia entre 2019 y 2021:
## diferencia de votos
right_vote_diff = right_votes_2021 - right_votes_2019
left_vote_diff = left_votes_2021 - left_votes_2019
## diferencia de porcentajes
right_percentage_diff = right_percentage_2021 - right_percentage_2019
left_percentage_diff = left_percentage_2021 - left_percentage_2019
import plotly.graph_objects as go
parties=['derecha', 'izquierda']
final_results_2019 = [
right_votes_2019,
left_votes_2019,
]
final_results_2021 = [
right_votes_2021,
left_votes_2021,
]
fig = go.Figure(data=[
go.Bar(name='2021', x=parties, y=final_results_2021),
go.Bar(name='2019', x=parties, y=final_results_2019)
])
# Change the bar mode
fig.update_layout(title='Derecha (vs) Izquierda', barmode='group')
fig.show()
# 2019
## Derecha:
right_votes_2019 = str("{:,}".format(right_votes_2019)).strip('.0')+' votos'
right_percentage_2019 = str(round(right_percentage_2019, 2))+' %'
## Izquierda:
left_votes_2019 = str("{:,}".format(left_votes_2019)).strip('.0')+' votos'
left_percentage_2019 = str(round(left_percentage_2019, 2))+' %'
## Derecha-Izquierda diferencia
right_left_vote_diff_2019 = str("{:,}".format(right_left_vote_diff_2019)).strip('.0')+' votos'
right_left_perc_diff_2019 = str(round(right_left_perc_diff_2019, 2))+' %'
# 2021
## Derecha:
right_votes_2021 = str("{:,}".format(right_votes_2021)).strip('.0')+' votos'
right_percentage_2021 = str(round(right_percentage_2021, 2))+' %'
## Izquierda:
left_votes_2021 = str("{:,}".format(left_votes_2021)).strip('.0')+' votos'
left_percentage_2021 = str(round(left_percentage_2021, 2))+' %'
## Derecha-Izquierda diferencia
right_left_vote_diff_2021 = str("{:,}".format(right_left_vote_diff_2021)).strip('.0')+' votos'
right_left_perc_diff_2021 = str(round(right_left_perc_diff_2021, 2))+' %'
# Diferencia entre 2019-2021
## diferencia de votos
right_vote_diff = str("{:,}".format(right_vote_diff)).strip('.0')+' votos'
left_vote_diff = str("{:,}".format(left_vote_diff)).strip('.0')+' votos'
## diferencia de porcentajes
right_percentage_diff = str(round(right_percentage_diff, 2))+' %'
left_percentage_diff = str(round(left_percentage_diff, 2))+' %'
print('RIGHT_2019: '+ right_votes_2019 +' / '+ right_percentage_2019)
print('RIGHT_2021: '+ right_votes_2021 +' / '+ right_percentage_2021)
print('RIGHT_DIFF: '+ right_vote_diff +' / '+ right_percentage_diff)
RIGHT_2019: 1,523,768 votos / 47.1 %
RIGHT_2021: 2,010,439 votos / 55.16 %
RIGHT_DIFF: 486,671 votos / 8.06 %
print('LEFT_2019: '+ left_votes_2019 +' / '+ left_percentage_2019)
print('LEFT_2021: '+ left_votes_2021 +' / '+ left_percentage_2021)
print('LEFT_DIFF: '+ left_vote_diff +' / '+ left_percentage_diff)
LEFT_2019: 1,533,148 votos / 47.39 %
LEFT_2021: 1,485,86 votos / 40.77 %
LEFT_DIFF: -47,288 votos / -6.62 %
print('RIGHT_LEFT_DIFF_2019: '+ right_left_vote_diff_2019 +' / '+ right_left_perc_diff_2019)
print('RIGHT_LEFT_DIFF_2021: '+ right_left_vote_diff_2021 +' / '+ right_left_perc_diff_2021)
RIGHT_LEFT_DIFF_2019: -9,38 votos / -0.29 %
RIGHT_LEFT_DIFF_2021: 524,579 votos / 14.39 %
Podemos decir que el bloque de la derecha aumenta en 490,792 votos (8.07%) en 2021, mientras que el bloque de la izquierda cae en 47,468 votos (-6.63%). En 2019, la derecha representaba el 47.11% (1,533,343 votos) mientras que la izquierda un 47.36% (1,541,502 votos), prácticamente empatados con una diferencia del 0.25% (8,159 votos) a favor de la izquierda. En cambio en el 2021 la derecha representa un 55.18% (2,024,135 votos) frente al 40.73% (1,494,034 votos), una diferencia del 14.45% (530,101 votos) a favor de la derecha.
El aumento de la derecha está liderado por el crecimiento del PP, pero se ve atenuado por la fuerte caída de Cs y el poco crecimiento de Vox. Mientras tanto en la izquierda la caída se debe al desplome del PSOE principalmente, pero atenuado por los crecimientos de Más Madrid y Podemos-IU.
2.3.-Distribución espacial de los resultados¶
A continuación vamos a hacer representaciones espaciales sobre la distribución del voto por los municipios de Madrid. Se van a seguir comparativas parecidas a las del apartado anterior: PP (vs) PSOE, PP-PSOE (vs) Otros Partidos, Derecha (vs) Izquierda, comparación del PP con el resto de partidos.
Para la construcción de mapas interactivos que permitieran mostrar la distribución del voto en cada municipio se ha utilizado la librería BokehJS y los datos geoespaciales de nuestros dataframes (columna ‘geometry’).
# funcion que te devuelve nº de municipios donde gana un partido (party_1) sobre otro (party_2):
def won_municipalities(party_1, party_2, df):
municipalities = []
municipalities = df['municipio'][df[party_1] > df[party_2]].count()
return municipalities
PP (vs) PSOE
# añado nuevas columnas
df_2019["pp_share"] = df_2019["pp"] / df_2019["votos_totales"]
df_2019["rel_pp_share"] = df_2019["pp"] / (df_2019["pp"]+df_2019["psoe"])
df_2019["psoe_share"] = df_2019["psoe"] / df_2019["votos_totales"]
df_2019["rel_psoe_share"] = df_2019["psoe"] / (df_2019["pp"]+df_2019["psoe"])
from geopandas import GeoDataFrame
# result
from bokeh.io import output_notebook
from bokeh.plotting import figure, ColumnDataSource
from bokeh.io import output_notebook, show, output_file
from bokeh.plotting import figure
from bokeh.models import GeoJSONDataSource, LinearColorMapper, ColorBar, HoverTool
from bokeh.palettes import brewer
output_notebook()
import json
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
('psoe','@psoe'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: PP (vs) PSOE", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_psoe_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# añado nuevas columnas
df_2021["pp_share"] = df_2021["pp"] / df_2021["votos_totales"]
df_2021["rel_pp_share"] = df_2021["pp"] / (df_2021["pp"]+df_2021["psoe"])
df_2021["psoe_share"] = df_2021["psoe"] / df_2021["votos_totales"]
df_2021["rel_psoe_share"] = df_2021["psoe"] / (df_2021["pp"]+df_2021["psoe"])
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
('psoe','@psoe'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: PP (vs) PSOE", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_psoe_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por el PP al PSOE en 2019
won_municipalities('pp', 'psoe', df_2019)
63
# municipios ganados por el PSOE al PP en 2019
won_municipalities('psoe', 'pp', df_2019)
114
# municipios ganados por el PP al PSOE en 2021
won_municipalities('pp', 'psoe', df_2021)
176
# municipios ganados por el PSOE al PP en 2021
won_municipalities('psoe', 'pp', df_2021)
2
Se puede apreciar la gran debacle del PSOE al pasar de 114 municipios en los que obtuvo más apoyos sobre el PP en 2019 a solo 2 en 2021, mientras que el PP pasa de 63 municipios en 2019 a 176 en 2021. En el municipio de Navarredonda y San Mamés se obtiene un empate técnico entre ambos partidos.
PP-PSOE (vs) Otros Partidos
# 2019
df_2019 = df_2019.fillna(0)
df_2019['pp_psoe'] = df_2019["pp"] + df_2019["psoe"]
df_2019['otros_partidos'] = df_2019["mas_madrid"] + df_2019["cs"] + df_2019["podemos_iu"] + df_2019["vox"]
df_2019["pp_psoe_share"] = df_2019["pp_psoe"] / df_2019["votos_totales"]
df_2019["rel_pp_psoe_share"] = df_2019["pp_psoe"] / (df_2019["pp"]+df_2019["otros_partidos"])
df_2019["otros_partidos_share"] = df_2019["otros_partidos"] / df_2019["votos_totales"]
df_2019["rel_otros_partidos_share"] = df_2019["otros_partidos"] / (df_2019["pp_psoe"]+df_2019["otros_partidos"])
# 2021
df_2021 = df_2021.fillna(0)
df_2021['pp_psoe'] = df_2021["pp"] + df_2021["psoe"]
df_2021['otros_partidos'] = df_2021["mas_madrid"] + df_2021["cs"] + df_2021["podemos_iu"] + df_2021["vox"]
df_2021["pp_psoe_share"] = df_2021["pp_psoe"] / df_2021["votos_totales"]
df_2021["rel_pp_psoe_share"] = df_2021["pp_psoe"] / (df_2021["pp"]+df_2021["otros_partidos"])
df_2021["otros_partidos_share"] = df_2021["otros_partidos"] / df_2021["votos_totales"]
df_2021["rel_otros_partidos_share"] = df_2021["otros_partidos"] / (df_2021["pp_psoe"]+df_2021["otros_partidos"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp_psoe', '@pp_psoe'),
('otros_partidos','@otros_partidos'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: PP-PSOE (vs) Otros Partidos", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_otros_partidos_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp_psoe', '@pp_psoe'),
('otros_partidos','@otros_partidos'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: PP-PSOE (vs) Otros Partidos", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_otros_partidos_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por PP-PSOE al resto de partidos en 2019
won_municipalities('pp_psoe', 'otros_partidos', df_2019)
121
# municipios ganados por el resto de partidos a PP-PSOE en 2019
won_municipalities('otros_partidos', 'pp_psoe', df_2019)
58
# municipios ganados por PP-PSOE al resto de partidos en 2021
won_municipalities('pp_psoe', 'otros_partidos', df_2021)
176
# municipios ganados por el resto de partidos a PP-PSOE en 2021
won_municipalities('otros_partidos', 'pp_psoe', df_2021)
3
Se ve la reconfiguración del eje PP-PSOE frente a otros partidos (Cs, Vox, Podemos-IU y Más Madrid), ya que se pasa de 121 municipios donde obtienen mayoría frente a 58 a 176 municipios frente a 3.
Derecha (vs) Izquierda
# 2019
df_2019['derecha'] = df_2019["pp"] + df_2019["cs"] + df_2019["vox"]
df_2019['izquierda'] = df_2019["psoe"]+ df_2019["mas_madrid"] + df_2019["podemos_iu"]
df_2019["derecha_share"] = df_2019["derecha"] / df_2019["votos_totales"]
df_2019["rel_derecha_share"] = df_2019["derecha"] / (df_2019["derecha"]+df_2019["izquierda"])
df_2019["izquierda_share"] = df_2019["izquierda"] / df_2019["votos_totales"]
df_2019["rel_izquierda_share"] = df_2019["izquierda"] / (df_2019["derecha"]+df_2019["izquierda"])
# 2021
df_2021['derecha'] = df_2021["pp"] + df_2021["cs"] + df_2021["vox"]
df_2021['izquierda'] = df_2021["psoe"]+ df_2021["mas_madrid"] + df_2021["podemos_iu"]
df_2021["derecha_share"] = df_2021["derecha"] / df_2021["votos_totales"]
df_2021["rel_derecha_share"] = df_2021["derecha"] / (df_2021["derecha"]+df_2021["izquierda"])
df_2021["izquierda_share"] = df_2021["izquierda"] / df_2021["votos_totales"]
df_2021["rel_izquierda_share"] = df_2021["izquierda"] / (df_2021["derecha"]+df_2021["izquierda"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('derecha', '@derecha'),
('izquierda','@izquierda'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: Derecha (vs) Izquierda", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_izquierda_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('derecha', '@derecha'),
('izquierda','@izquierda'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: Derecha (vs) Izquierda", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_izquierda_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por el eje de la derecha al eje de la izquierda en 2019
won_municipalities('derecha', 'izquierda', df_2019)
127
# municipios ganados por el eje de la izquierda al eje de la derecha en 2019
won_municipalities('izquierda', 'derecha', df_2019)
51
# municipios ganados por el eje de la derecha al eje de la izquierda en 2021
won_municipalities('derecha', 'izquierda', df_2021)
159
# municipios ganados por el eje de la izquierda al eje de la derecha en 2021
won_municipalities('izquierda', 'derecha', df_2021)
19
Se puede apreciar el gran avance general de la derecha en la Comunidad de Madrid: pasa de 127 municipios en 2019 a 159 en 2021; mientras que la izquierda pasa de 51 a solo 19 municipios.
PP (vs) Más Madrid
# 2019
df_2019["pp_share"] = df_2019["pp"] / df_2019["votos_totales"]
df_2019["rel_pp_share"] = df_2019["pp"] / (df_2019["pp"]+df_2019["mas_madrid"])
df_2019["mas_madrid_share"] = df_2019["mas_madrid"] / df_2019["votos_totales"]
df_2019["rel_mas_madrid_share"] = df_2019["mas_madrid"] / (df_2019["pp"]+df_2019["mas_madrid"])
# 2021
df_2021["pp_share"] = df_2021["pp"] / df_2021["votos_totales"]
df_2021["rel_pp_share"] = df_2021["pp"] / (df_2021["pp"]+df_2021["mas_madrid"])
df_2021["mas_madrid_share"] = df_2021["mas_madrid"] / df_2021["votos_totales"]
df_2021["rel_mas_madrid_share"] = df_2021["mas_madrid"] / (df_2021["pp"]+df_2021["mas_madrid"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
('mas_madrid','@mas_madrid'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: PP (vs) Más Madrid", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('pp', '@pp'),
('mas_madrid','@mas_madrid'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: PP (vs) Más Madrid", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por el PP a Más Madrid en 2019
won_municipalities('pp', 'mas_madrid', df_2019)
169
# municipios ganados por Más Madrid a el PP en 2019
won_municipalities('mas_madrid', 'pp', df_2019)
10
# municipios ganados por el PP a Más Madrid en 2019
won_municipalities('pp', 'mas_madrid', df_2021)
179
# municipios ganados por Más Madrid a el PP en 2019
won_municipalities('mas_madrid', 'pp', df_2021)
0
Más Madrid, que fue el partido dentro del eje de izquierdas y que consiguió dar el sorpaso al PSOE, pasó de tener 10 municipios donde sacó más votos que el PP en las elecciones de 2019 a ninguno en las elecciones de 2021. Por lo tanto, a pesar del sorpaso, el PP mantiene el tipo frente a todos los partidos de izquierdas.
Es importante destacar cómo Más Madrid ha conseguido tener su caladero de votos en los feudos tradicionales del PSOE (sureste del municipio de Madrid: Rivas, Coslada, San Fernando, Fuenlabrada, Getafe, Parla, Pinto y Mejorada del Campo).
Más Madrid (vs) Podemos-IU
Por último vamos a ver dentro del eje de izquierdas la distribución del voto a la izquierda del PSOE entre Podemos-IU y Más Madrid.
# 2019
df_2019["podemos_iu_share"] = df_2019["podemos_iu"] / df_2019["votos_totales"]
df_2019["rel_podemos_iu_share"] = df_2019["podemos_iu"] / (df_2019["podemos_iu"]+df_2019["mas_madrid"])
df_2019["mas_madrid_share"] = df_2019["mas_madrid"] / df_2019["votos_totales"]
df_2019["rel_mas_madrid_share"] = df_2019["mas_madrid"] / (df_2019["podemos_iu"]+df_2019["mas_madrid"])
# 2021
df_2021["podemos_iu_share"] = df_2021["podemos_iu"] / df_2021["votos_totales"]
df_2021["rel_podemos_iu_share"] = df_2021["podemos_iu"] / (df_2021["podemos_iu"]+df_2021["mas_madrid"])
df_2021["mas_madrid_share"] = df_2021["mas_madrid"] / df_2021["votos_totales"]
df_2021["rel_mas_madrid_share"] = df_2021["mas_madrid"] / (df_2021["podemos_iu"]+df_2021["mas_madrid"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('podemos_iu', '@podemos_iu'),
('mas_madrid','@mas_madrid'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: Más Madrid (vs) Podemos-IU", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('podemos_iu', '@podemos_iu'),
('mas_madrid','@mas_madrid'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: Más Madrid (vs) Podemos-IU", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_mas_madrid_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
# municipios ganados por Más Madrid a Podemos-IU en 2019
won_municipalities('mas_madrid', 'podemos_iu', df_2019)
160
# municipios ganados por Podemos-IU a Más Madrid en 2019
won_municipalities('podemos_iu', 'mas_madrid', df_2019)
17
# municipios ganados por Más Madrid a Podemos-IU en 2021
won_municipalities('mas_madrid', 'podemos_iu', df_2021)
173
# municipios ganados por Podemos-IU a Más Madrid en 2021
won_municipalities('podemos_iu', 'mas_madrid', df_2021)
6
Vemos cómo el apoyo a Más Madrid crece entre las dos elecciones con respecto a Podemos-IU, ya que pasa de 160 municipios donde suma más votos frente a 17 a 173 contra 6. Esto puede reflejar que el ser parte del Gobierno a nivel nacional le ha pasado factura a Podemos-IU.
Destaca cómo Más Madrid es la opción favorita frente a Podemos-IU sobre todo en los principales núcleos urbanos, mientras que Podemos-IU consigue superar a Más Madrid en pequeños municipios alejados del municipio de Madrid, sobre todo en el norte.
2.4.-Análisis de la abstención¶
A continuación vamos a analizar la evolución de la abstención entre las 2 elecciones para ver hasta qué grado pudo influir entre un resultado u otro.
Definimos unas funciones para obtener la abstención electoral en votos y porcentajes entre las dos elecciones:
def electoral_abstention(df_year):
abstention_total_votes = df_year['abstencion'].sum()
abstention_total_percentage = (df_year['abstencion'].sum()/df_year['votos_totales'].sum())*100
abstention_total_votes = str("{:,}".format(abstention_total_votes)).strip('.0')+' votos'
abstention_total_percentage = str(round(abstention_total_percentage, 2))+' %'
return [abstention_total_votes, abstention_total_percentage]
def dif_electoral_abstention(df_year_1, df_year_2):
abstention_total_votes_1 = df_year_1['abstencion'].sum()
abstention_total_percentage_1 = (df_year_1['abstencion'].sum()/df_year_1['votos_totales'].sum())*100
abstention_total_votes_2 = df_year_2['abstencion'].sum()
abstention_total_percentage_2 = (df_year_2['abstencion'].sum()/df_year_2['votos_totales'].sum())*100
diff_abstention_votes = abstention_total_votes_1 - abstention_total_votes_2
diff_abstention_percentage = abstention_total_percentage_1 - abstention_total_percentage_2
abstencion_total_votes = str("{:,}".format(diff_abstention_votes)).strip('.0')+' votos'
abstention_total_percentage = str(round(diff_abstention_percentage, 2))+' %'
return [abstencion_total_votes, abstention_total_percentage]
print('Abstención 2019: '+ electoral_abstention(df_2019)[0] +' / '+ electoral_abstention(df_2019)[1])
print('Abstención 2021: '+ electoral_abstention(df_2021)[0] +' / '+ electoral_abstention(df_2021)[1])
print('Diferencia 2021-2019: '+ dif_electoral_abstention(df_2021, df_2019)[0] +' / '+ dif_electoral_abstention(df_2021, df_2019)[1])
Abstención 2019: 1,516,826 votos / 46.89 %
Abstención 2021: 1,135,201 votos / 31.15 %
Diferencia 2021-2019: -381,625 votos / -15.74 %
Podemos ver cómo la abstención pasa de representar el 46.89% del total en las elecciones del 2019 (1,516,826 votos) a el 31.15% (1,135,201 votos), lo que supone una caída porcentual del 15.74% (-381,625 votos). Usualmente se atribuyen una menor a abstención a una mayor movilización de la izquierda, pero aquí vemos lo contrario: en 2019 obtiene más votos el candidato del PSOE, mientras que en 2021 la candidata del PP.
Distribución espacial de la abstención¶
Vamos a representar cómo se distribuye la abstención sobre el mapa y analizaremos qué partido sale beneficiado de la reducción/aumento de la abstención en cada municipio:
# 2019
df_2019["abstencion_share"] = df_2019["abstencion"] / df_2019["votos_totales"]
df_2019["rel_abstencion_share"] = df_2019["abstencion"] / (df_2019["abstencion"]+df_2019["votos_totales"])
df_2019["votos_totales_share"] = df_2019["votos_totales"] / df_2019["votos_totales"]
df_2019["rel_votos_totales_share"] = df_2019["votos_totales"] / (df_2019["abstencion"]+df_2019["votos_totales"])
# 2021
df_2021["abstencion_share"] = df_2021["abstencion"] / df_2021["votos_totales"]
df_2021["rel_abstencion_share"] = df_2021["abstencion"] / (df_2021["abstencion"]+df_2021["votos_totales"])
df_2021["votos_totales_share"] = df_2021["votos_totales"] / df_2021["votos_totales"]
df_2021["rel_votos_totales_share"] = df_2021["votos_totales"] / (df_2021["abstencion"]+df_2021["votos_totales"])
result = GeoDataFrame(df_2019)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('votos_totales', '@votos_totales'),
('abstencion','@abstencion'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2019: distribución de la abstención", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_votos_totales_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
result = GeoDataFrame(df_2021)
wi_geojson=GeoJSONDataSource(geojson=result.to_json())
color_mapper = LinearColorMapper(palette = brewer['RdBu'][10], low = 0, high = 1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=8,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal')
hover = HoverTool(tooltips = [ ('municipio','@municipio'),('votos_totales', '@votos_totales'),
('abstencion','@abstencion'),
('votos_totales','@votos_totales')])
p = figure(title="Elecciones Madrid 2021: distribución de la abstención", tools=[hover])
p.patches("xs","ys",source=wi_geojson,
fill_color = {'field' :'rel_votos_totales_share', 'transform' : color_mapper})
p.add_layout(color_bar, 'below')
show(p)
Podemos ver en 2019 cómo la abstención se centra sobre todo en grandes núcleos de población (municipio de Madrid y contiguos), así como en municipios del sur (p.ej. Colmenar de Oreja). Contrasta la alta participación de los pequeños municipios del norte de Madrid, Valdemaqueda, Valdaracete, Santorcaz y Olmeda de las Fuentes.
En cambio en 2021 podemos ver cómo la abstención general se reduce en los grandes núcleos de población, mientras se reduce en algunos municipios del norte.
import pandas as pd
df_abstenciones = pd.DataFrame()
df_abstenciones['municipio'] = df_2021['municipio']
df_abstenciones['votos_totales_2019'] = df_2019['votos_totales']
df_abstenciones['votos_totales_2021'] = df_2021['votos_totales']
df_abstenciones['abstencion_2019'] = df_2019['abstencion']
df_abstenciones['abstencion_2021'] = df_2021['abstencion']
df_abstenciones['dif_abstencion_votos'] = df_abstenciones['abstencion_2021'] - df_abstenciones['abstencion_2019']
df_abstenciones['dif_abstencion_porcentaje'] = df_2021['abstencion_porcentaje'] - df_2019['abstencion_porcentaje']
df_abstenciones.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 179 entries, 0 to 178
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 municipio 179 non-null object
1 votos_totales_2019 179 non-null float64
2 votos_totales_2021 179 non-null float64
3 abstencion_2019 179 non-null float64
4 abstencion_2021 179 non-null float64
5 dif_abstencion_votos 179 non-null float64
6 dif_abstencion_porcentaje 179 non-null float64
dtypes: float64(6), object(1)
memory usage: 11.2+ KB
# donde más crece la abstención en 2021 con respecto a 2019:
df_abstenciones.nlargest(15, ['dif_abstencion_porcentaje'])
| municipio | votos_totales_2019 | votos_totales_2021 | abstencion_2019 | abstencion_2021 | dif_abstencion_votos | dif_abstencion_porcentaje | |
|---|---|---|---|---|---|---|---|
| 138 | Somosierra | 71.0 | 58.0 | 6.0 | 18.0 | 12.0 | 15.89 |
| 74 | La Hiruela | 55.0 | 51.0 | 5.0 | 14.0 | 9.0 | 13.21 |
| 117 | Puebla de la Sierra | 54.0 | 50.0 | 7.0 | 16.0 | 9.0 | 12.76 |
| 151 | Valdaracete | 434.0 | 385.0 | 51.0 | 108.0 | 57.0 | 11.39 |
| 99 | Navarredonda y San Mamés | 107.0 | 102.0 | 19.0 | 34.0 | 15.0 | 9.92 |
| 124 | Robledillo de la Jara | 51.0 | 58.0 | 9.0 | 19.0 | 10.0 | 9.68 |
| 47 | El Atazar | 65.0 | 64.0 | 14.0 | 24.0 | 10.0 | 9.55 |
| 72 | La Acebeda | 70.0 | 42.0 | 7.0 | 9.0 | 2.0 | 8.56 |
| 54 | Estremera | 708.0 | 677.0 | 204.0 | 302.0 | 98.0 | 8.48 |
| 116 | Prádena del Rincón | 89.0 | 100.0 | 8.0 | 20.0 | 12.0 | 8.42 |
| 111 | Pinilla del Valle | 145.0 | 140.0 | 15.0 | 29.0 | 14.0 | 7.78 |
| 18 | Braojos | 152.0 | 150.0 | 14.0 | 29.0 | 15.0 | 7.77 |
| 81 | Lozoya | 374.0 | 367.0 | 53.0 | 90.0 | 37.0 | 7.28 |
| 32 | Cervera de Buitrago | 108.0 | 104.0 | 21.0 | 31.0 | 10.0 | 6.68 |
| 126 | Robregordo | 33.0 | 35.0 | 14.0 | 19.0 | 5.0 | 5.40 |
# donde más se reduce la abstención en 2021 con respecto a 2019:
df_abstenciones.nsmallest(15, ['dif_abstencion_porcentaje'])
| municipio | votos_totales_2019 | votos_totales_2021 | abstencion_2019 | abstencion_2021 | dif_abstencion_votos | dif_abstencion_porcentaje | |
|---|---|---|---|---|---|---|---|
| 167 | Villalbilla | 6579.0 | 8628.0 | 3186.0 | 1925.0 | -1261.0 | -14.39 |
| 57 | Fuenlabrada | 88021.0 | 105973.0 | 53332.0 | 35349.0 | -17983.0 | -12.72 |
| 12 | Arroyomolinos | 13661.0 | 16979.0 | 6875.0 | 4455.0 | -2420.0 | -12.70 |
| 45 | Cubas de la Sagra | 2808.0 | 3474.0 | 1569.0 | 1075.0 | -494.0 | -12.22 |
| 101 | Nuevo Baztán | 2729.0 | 3426.0 | 1692.0 | 1219.0 | -473.0 | -12.03 |
| 105 | Parla | 45596.0 | 55006.0 | 32908.0 | 23837.0 | -9071.0 | -11.69 |
| 65 | Griñón | 5044.0 | 6019.0 | 2492.0 | 1685.0 | -807.0 | -11.20 |
| 11 | Arganda del Rey | 22079.0 | 26481.0 | 12051.0 | 8455.0 | -3596.0 | -11.11 |
| 94 | Móstoles | 97110.0 | 113817.0 | 56442.0 | 39308.0 | -17134.0 | -11.09 |
| 16 | Berzosa del Lozoya | 97.0 | 116.0 | 84.0 | 64.0 | -20.0 | -10.85 |
| 44 | Coslada | 39726.0 | 45394.0 | 18155.0 | 11903.0 | -6252.0 | -10.60 |
| 71 | Humanes de Madrid | 8203.0 | 9619.0 | 4523.0 | 3233.0 | -1290.0 | -10.38 |
| 6 | Algete | 9817.0 | 11432.0 | 4698.0 | 3227.0 | -1471.0 | -10.36 |
| 133 | San Sebastián de los Reyes | 42567.0 | 49890.0 | 20915.0 | 14572.0 | -6343.0 | -10.34 |
| 25 | Camarma de Esteruelas | 3268.0 | 3900.0 | 1779.0 | 1296.0 | -483.0 | -10.31 |
Como podemos confirmar con la creación de este nuevo dataframe que resume la abstención entre las elecciones de 2019 y 2021: la abstención crece más en pequeños municipios y más en municipios que ya empiezan a tener una población considerable.
3.- Conclusiones¶
Después de haber analizado los resultados de las elecciones de 2019 y 2021 podemos concluir lo siguiente:
El PP es el indiscutible ganador de las elecciones de 2021 al ser el partido que más crece de todos los que consiguieron representación parlamentaria. La estrategia de Isabel Díaz Ayuso de anular a nivel interno a la oposición y convertirse a nivel nacional en el principal agente de oposición a la gestión de la pandemia por parte del Gobierno nacional, por encima incluso del líder a nivel nacional del PP, le ha reportado grandes beneficios.
El PSOE es el gran perdedor no solo por la pérdida de votos, sino también por la pérdida de apoyos tradicionales entre su electorado que ha optado por el voto castigo hacia el PP o la búsqueda de una nueva opción: Más Madrid. Muchos analistas coinciden en que la pérdida de apoyo viene de una falta de liderazgo por parte de Ángel Gabilondo frente a Isabel Díaz Ayuso, así como ser el partido a nivel nacional que ha gestionado la pandemia de COVID-19.
Ciudadanos sufre las consecuencias de formar parte de un gobierno de coalición donde no han sabido hacer frente al liderazgo de Isabel Díaz Ayuso, así como las consecuencias de una política errática a nivel nacional en la que no ha podido hacer frente al PP y al auge de Vox en sus campañas contra el Gobierno nacional en diferentes problemáticas nacionales (Cataluña, COVID-19, etc).
Más Madrid es el partido de izquierdas que cosecha más éxitos al aumentar su base electoral y conseguir dar el deseado sorpaso al PSOE que partidos como Podemos-IU han intentado y nunca han conseguido. Se ha beneficiado de ser un partido de izquierdas que no forma parte del Gobierno nacional de PSOE-UnidasPodemos. Por último, desde diferentes analistas se ha dicho que su candidata, Mónica García, era la candidata perfecta para el momento histórico que se vivía: una profesional sanitaria en el contexto de la pandemia de COVID-19.
Podemos-IU consigue aumentar mínimamente sus apoyos electorales por la participación de su líder fundador como candidato, Pablo Iglesias Turrión. Indudablemente ha conseguido movilizar a su base electoral para recuperar apoyos perdidos, ya que todas las encuestas indicaban que podían incluso desaparecer como Cs, pero aún así cabe preguntarse si su candidatura movilizó también el voto contra la izquierda a nivel general.
Vox a pesar de que en las pasadas elecciones de 2019 mostraron un fuerte músculo para poder crecer y situarse en las instituciones madrileñas, tampoco han podido escapar del liderazgo de Isabel Díaz Ayuso. Consiguen mantener su base de apoyo, pero no consiguen hacerla crecer ni un 1%.
En cuanto a la participación, las elecciones de 2021 han desmitificado que una participación alta sea sinónimo de un mayor apoyo hacia la izquierda vistos los resultados. Se podría decir que esta mayor movilización se ha debido a los liderazgos principales de Isabel Díaz Ayuso y Pablo Iglesias Turrión, que han movilizado el voto a uno y otro lado.
!pip list
Package Version
---------------------------------- -------------------
alabaster 0.7.12
anaconda-client 1.7.2
anaconda-navigator 2.0.3
anaconda-project 0.9.1
anyio 2.2.0
appdirs 1.4.4
argh 0.26.2
argon2-cffi 20.1.0
asn1crypto 1.4.0
astroid 2.5
astropy 4.2.1
async-generator 1.10
atomicwrites 1.4.0
attrs 20.3.0
autopep8 1.5.6
Babel 2.9.0
backcall 0.2.0
backports.functools-lru-cache 1.6.4
backports.shutil-get-terminal-size 1.0.0
backports.tempfile 1.0
backports.weakref 1.0.post1
beautifulsoup4 4.9.3
bitarray 2.1.0
bkcharts 0.2
black 19.10b0
bleach 3.3.0
bokeh 2.3.2
boto 2.49.0
Bottleneck 1.3.2
brotlipy 0.7.0
certifi 2020.12.5
cffi 1.14.5
chardet 4.0.0
chart-studio 1.1.0
click 7.1.2
click-plugins 1.1.1
cligj 0.7.2
cloudpickle 1.6.0
clyent 1.2.2
colorama 0.4.4
conda 4.10.1
conda-build 3.21.4
conda-content-trust 0+unknown
conda-package-handling 1.7.3
conda-repo-cli 1.0.4
conda-token 0.3.0
conda-verify 3.4.2
contextlib2 0.6.0.post1
cryptography 3.4.7
cycler 0.10.0
Cython 0.29.23
cytoolz 0.11.0
dask 2021.4.0
decorator 5.0.6
defusedxml 0.7.1
diff-match-patch 20200713
distributed 2021.4.1
docutils 0.16
entrypoints 0.3
et-xmlfile 1.0.1
fastcache 1.1.0
filelock 3.0.12
Fiona 1.8.20
flake8 3.9.0
Flask 1.1.2
fsspec 0.9.0
future 0.18.2
geopandas 0.9.0
gevent 21.1.2
ghp-import 2.0.2
gitdb 4.0.7
GitPython 3.1.24
glob2 0.7
gmpy2 2.0.8
greenlet 1.0.0
h5py 2.10.0
HeapDict 1.0.1
html5lib 1.1
idna 2.10
imageio 2.9.0
imagesize 1.2.0
importlib-metadata 3.10.0
importlib-resources 5.2.2
iniconfig 1.1.1
intervaltree 3.1.0
ipykernel 5.3.4
ipython 7.22.0
ipython-genutils 0.2.0
ipywidgets 7.6.3
isort 5.8.0
itsdangerous 1.1.0
jdcal 1.4.1
jedi 0.17.2
jeepney 0.6.0
Jinja2 2.11.3
joblib 1.0.1
json5 0.9.5
jsonschema 3.2.0
jupyter 1.0.0
jupyter-book 0.11.3
jupyter-cache 0.4.3
jupyter-client 6.1.12
jupyter-console 6.4.0
jupyter-core 4.7.1
jupyter-packaging 0.7.12
jupyter-server 1.4.1
jupyter-server-mathjax 0.2.3
jupyter-sphinx 0.3.2
jupyterlab 3.0.14
jupyterlab-pygments 0.1.2
jupyterlab-server 2.4.0
jupyterlab-widgets 1.0.0
jupytext 1.10.3
keyring 22.3.0
kiwisolver 1.3.1
latexcodec 2.0.1
lazy-object-proxy 1.6.0
libarchive-c 2.9
linkify-it-py 1.0.1
llvmlite 0.36.0
locket 0.2.1
lxml 4.6.3
markdown-it-py 0.6.2
MarkupSafe 1.1.1
matplotlib 3.3.4
mccabe 0.6.1
mdit-py-plugins 0.2.6
mistune 0.8.4
mkl-fft 1.3.0
mkl-random 1.2.1
mkl-service 2.3.0
mock 4.0.3
more-itertools 8.7.0
mpmath 1.2.1
msgpack 1.0.2
multipledispatch 0.6.0
munch 2.5.0
mypy-extensions 0.4.3
myst-nb 0.12.3
myst-parser 0.13.7
navigator-updater 0.2.1
nbclassic 0.2.6
nbclient 0.5.3
nbconvert 5.6.1
nbdime 3.1.0
nbformat 5.1.3
nest-asyncio 1.5.1
networkx 2.5
nltk 3.6.1
nose 1.3.7
notebook 6.3.0
numba 0.53.1
numexpr 2.7.3
numpy 1.20.1
numpydoc 1.1.0
olefile 0.46
openpyxl 3.0.7
packaging 20.9
pandas 1.2.4
pandocfilters 1.4.3
parso 0.7.0
partd 1.2.0
path 15.1.2
pathlib2 2.3.5
pathspec 0.7.0
patsy 0.5.1
pep8 1.7.1
pexpect 4.8.0
pickleshare 0.7.5
Pillow 8.2.0
pip 21.0.1
pkginfo 1.7.0
plotly 5.0.0
pluggy 0.13.1
ply 3.11
prometheus-client 0.10.1
prompt-toolkit 3.0.17
psutil 5.8.0
ptyprocess 0.7.0
py 1.10.0
pybtex 0.24.0
pybtex-docutils 1.0.1
pycodestyle 2.6.0
pycosat 0.6.3
pycparser 2.20
pycurl 7.43.0.6
pydata-sphinx-theme 0.6.3
pydocstyle 6.0.0
pyerfa 1.7.3
pyflakes 2.2.0
Pygments 2.8.1
pylint 2.7.4
pyls-black 0.4.6
pyls-spyder 0.3.2
pyodbc 4.0.0-unsupported
pyOpenSSL 20.0.1
pyparsing 2.4.7
pyproj 3.1.0
PyQt5 5.12
PyQt5-Qt5 5.15.2
PyQt5-sip 4.19.19
PyQtWebEngine 5.12
pyrsistent 0.17.3
PySocks 1.7.1
pytest 6.2.3
python-dateutil 2.8.1
python-jsonrpc-server 0.4.0
python-language-server 0.36.2
pytz 2021.1
PyWavelets 1.1.1
pyxdg 0.27
PyYAML 5.4.1
pyzmq 20.0.0
QDarkStyle 2.8.1
QtAwesome 1.0.2
qtconsole 5.0.3
QtPy 1.9.0
regex 2021.4.4
requests 2.25.1
retrying 1.3.3
rope 0.18.0
Rtree 0.9.7
ruamel-yaml-conda 0.15.100
scikit-image 0.18.1
scikit-learn 0.24.1
scipy 1.6.2
seaborn 0.11.1
SecretStorage 3.3.1
Send2Trash 1.5.0
setuptools 52.0.0.post20210125
Shapely 1.7.1
simplegeneric 0.8.1
singledispatch 0.0.0
sip 4.19.13
six 1.15.0
smmap 4.0.0
sniffio 1.2.0
snowballstemmer 2.1.0
sortedcollections 2.1.0
sortedcontainers 2.3.0
soupsieve 2.2.1
Sphinx 3.5.4
sphinx-book-theme 0.1.4
sphinx-comments 0.0.3
sphinx-copybutton 0.4.0
sphinx-external-toc 0.2.3
sphinx-jupyterbook-latex 0.4.2
sphinx-multitoc-numbering 0.1.3
sphinx-panels 0.5.2
sphinx-thebe 0.0.10
sphinx-togglebutton 0.2.3
sphinxcontrib-applehelp 1.0.2
sphinxcontrib-bibtex 2.2.1
sphinxcontrib-devhelp 1.0.2
sphinxcontrib-htmlhelp 1.0.3
sphinxcontrib-jsmath 1.0.1
sphinxcontrib-qthelp 1.0.3
sphinxcontrib-serializinghtml 1.1.4
sphinxcontrib-websupport 1.2.4
spyder 4.2.5
spyder-kernels 1.10.2
SQLAlchemy 1.4.15
statsmodels 0.12.2
sympy 1.8
tables 3.6.1
tblib 1.7.0
tenacity 7.0.0
terminado 0.9.4
testpath 0.4.4
textdistance 4.2.1
threadpoolctl 2.1.0
three-merge 0.1.1
tifffile 2020.10.1
toml 0.10.2
toolz 0.11.1
tornado 6.1
tqdm 4.59.0
traitlets 5.0.5
typed-ast 1.4.2
typing-extensions 3.7.4.3
uc-micro-py 1.0.1
ujson 4.0.2
unicodecsv 0.14.1
urllib3 1.26.4
watchdog 1.0.2
wcwidth 0.2.5
webencodings 0.5.1
Werkzeug 1.0.1
wheel 0.36.2
widgetsnbextension 3.5.1
wrapt 1.12.1
wurlitzer 2.1.0
xlrd 2.0.1
XlsxWriter 1.3.8
xlwt 1.3.0
xmltodict 0.12.0
yapf 0.31.0
zict 2.0.0
zipp 3.4.1
zope.event 4.5.0
zope.interface 5.3.0